Compare commits

..

242 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
Daniel Szabo
aec6cab275 Merge branch 'master' of https://github.com/szabodanika/microbin 2023-07-04 21:56:47 +03:00
Daniel Szabo
4983ac867d Fix text-only private pastas crashing server
- Fixed a bug that caused encrypted uploads with no file to crash the server, as we tried to decrypt a non-existent file
- Changed wording on pasta auth page
- Removed unused import
2023-07-04 21:56:15 +03:00
Daniel Szabo
d7b4462892 Fix edit saving bug
Fixes #177
2023-07-04 19:23:58 +03:00
Daniel Szabo
4c367c0ddf Bump version to 2.0.1 (beta2) 2023-07-04 19:23:23 +03:00
Daniel Szabo
352d75f2f6 Update reference to new fabvicon 2023-07-04 19:23:04 +03:00
Dániel Szabó
3c4d3a83b4
Update README.md
Added link to PTS
2023-07-02 23:59:32 +03:00
Daniel Szabo
c45bca7b05 Merge branch 'master' of https://github.com/szabodanika/microbin 2023-07-02 18:26:55 +03:00
Daniel Szabo
9cd8f455aa New favicon 2023-07-02 18:26:54 +03:00
Dániel Szabó
a10cde6033
Update README.md
updated docker script command
2023-07-02 13:13:27 +03:00
Daniel Szabo
2d66c1abf8 Changed MICROBIN_NO_ETERNAL_PASTA to MICROBIN_ETERNAL_PASTA 2023-07-02 13:04:45 +03:00
Daniel Szabo
7c88276a63 Docker setup script
Plus added gitignore for docker volume
2023-07-02 13:04:28 +03:00
Daniel Szabo
b414e1f11f Merge branch 'master' of https://github.com/szabodanika/microbin 2023-07-01 23:18:10 +03:00
Daniel Szabo
49326f3c52 Update Dockerfile 2023-07-01 23:18:06 +03:00
Daniel Szabo
4eaa259f49 Update release.yml 2023-07-01 22:36:39 +03:00
Dániel Szabó
8e8765dd2e
Update README.md 2023-07-01 22:24:05 +03:00
Daniel Szabo
bbeaab7cf9 Update GitHub Actions
Removed duplicate GH release
Updated set-output commands
Bumped softprops/action-gh-release to v0.1.15
2023-07-01 22:16:34 +03:00
Daniel Szabo
41b565f81d Fix integer overflow issue
Project was not compiling on a few targets, there is no need for this unelegant TB calculation here.
2023-07-01 21:43:17 +03:00
Daniel Szabo
38e4fa4fe7 update cargo version number 2023-07-01 21:20:43 +03:00
Daniel Szabo
e05596726b Update guide.html 2023-07-01 21:06:06 +03:00
Daniel Szabo
4ad9d51aaa Removed link to non-existent /about 2023-07-01 20:51:47 +03:00
Daniel Szabo
3a1fc7072a Renamed "How to use" page to Guide page 2023-07-01 20:50:22 +03:00
Daniel Szabo
8c18e423a3 Fix sort path bug
- Fixed a bug that caused unset short_path variable to crash the server
- Touched up .env file
2023-07-01 20:49:55 +03:00
Daniel Szabo
eda06a8df7 New logo and index images 2023-07-01 20:30:26 +03:00
Daniel Szabo
79c2b358ba Applied clippy suggestions 2023-07-01 20:06:49 +03:00
Daniel Szabo
6913beed3b Update .gitignore 2023-07-01 19:35:09 +03:00
Daniel Szabo
e87603ac2c Update README.md
Preparing for changes in v2
2023-07-01 19:34:14 +03:00
Daniel Szabo
647d030aea Update default .env
Fixed incorrect default values
2023-07-01 19:34:00 +03:00
Daniel Szabo
bc188e3d34 Microbin 2 beta 1 first commit
This is the first, early beta version of Microbin 2.

Microbin 2 brings a lot of improvements to Microbin. To name just the most important ones:
- Server-side and client-side encryption for uploads, including file attachments,
- SQLite database support by default,
- Reworked upload settings, 5 privacy levels,
- New administrator console (/admin),
- Automatic syntax highlighting,
- Image and video embedding,
- Refreshed styling and QoL features,
- New user guide page.

Beyond these,
- Added file upload size limits,
- Added sample .env file with description for each environmental variable,
- Removed Info page (moved to admin page),
- Pasta list now sorted from newer to older,
- Pasta terminology replaced with Upload,
- Files are now served with a proper service, not just as a public directory,
- Pastas are now stored in /attachments instead of /public.

As this is the first beta of Microbin v2, and I do not recommend updating to this in critical environments as long as we are in beta stage. I expect many bugs to emerge and v2 is also getting a few more smaller QoL features from the backlog. README and website won't be updated until v2 is stable and well-tested.

Fixes #164
Fixes #156
Fixes #153
Fixes #149
Fixes #139
Fixes #98
Fixes #4

Fixes #105 (sort of)
Fixes #97 (sort of)
Fixes #45 (sort of)
Fixes #22 (sort of)
2023-07-01 19:12:06 +03:00
Dániel Szabó
3d45357be3
Merge pull request #169 from szabodanika/dependabot/cargo/tokio-1.25.1
Bump tokio from 1.21.2 to 1.25.1
2023-06-26 12:31:29 +03:00
Dániel Szabó
b0988d08c1
Merge pull request #168 from szabodanika/dependabot/cargo/spin-0.9.8
Bump spin from 0.9.4 to 0.9.8
2023-06-26 12:31:20 +03:00
Dániel Szabó
8fdfe0294b
Merge pull request #167 from szabodanika/dependabot/cargo/h2-0.3.19
Bump h2 from 0.3.15 to 0.3.19
2023-06-26 12:31:13 +03:00
dependabot[bot]
f581cdd969
Bump tokio from 1.21.2 to 1.25.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.21.2 to 1.25.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.21.2...tokio-1.25.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 07:45:41 +00:00
dependabot[bot]
85d8fa53a3
Bump spin from 0.9.4 to 0.9.8
Bumps [spin](https://github.com/mvdnes/spin-rs) from 0.9.4 to 0.9.8.
- [Changelog](https://github.com/mvdnes/spin-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mvdnes/spin-rs/commits)

---
updated-dependencies:
- dependency-name: spin
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 07:45:31 +00:00
dependabot[bot]
a5406867b6
Bump h2 from 0.3.15 to 0.3.19
Bumps [h2](https://github.com/hyperium/h2) from 0.3.15 to 0.3.19.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.15...v0.3.19)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-26 07:44:09 +00:00
Dániel Szabó
4f9c447dee
Merge pull request #154 from egor-tensin/dockerfile
Dockerfile: improve caching
2023-06-26 10:26:24 +03:00
Dániel Szabó
92a529dae3
Merge pull request #155 from egor-tensin/monospace
Use monospace font in textarea's
2023-06-26 10:24:10 +03:00
Dániel Szabó
2a15f40e71
Merge pull request #161 from mooons/dev-escape-backslash
Escape backslash for Copy Text function
2023-06-26 10:21:38 +03:00
mooons
83f3209740 Escape backslash 2023-05-25 17:30:13 -07:00
Egor Tensin
833454b343 Use monospace font in textarea's
This commit makes the textareas (notably, when adding a pasta) use
monospace font (which is in line with other pastebin services and
arguably what the user wants/expects).
2023-05-02 22:38:54 +02:00
Egor Tensin
f28f1e2b5c Dockerfile: improve caching
This improves caching in Docker builds somewhat so that apt-get is not
run on every change.
2023-05-02 22:37:06 +02:00
Dániel Szabó
b6841a596d
Merge pull request #151 from frap129/short-path
Add short-path for changing URL provided by copy buttons
2023-04-20 19:42:47 +03:00
Joe Maples
95aa514a30 Add shortened endpoints for using short_path 2023-04-12 11:29:52 -04:00
Joe Maples
25d199721b Add short_path arg, use it for copy urls 2023-04-12 11:15:19 -04:00
Dániel Szabó
0dbe9498f8
add crates.io badge url 2023-03-26 23:09:40 +03:00
Daniel Szabo
c9c60aa546 increment version number and update homepage in Cargo.toml 2023-03-26 23:07:07 +03:00
Daniel Szabo
0eedf1c8e0 increment version number in Cargo.lock 2023-03-26 23:06:46 +03:00
Dániel Szabó
a2c8eacbf9
Merge pull request #115 from jchia/fix-edit
Follow-up to 7522d419
2023-03-26 22:34:37 +03:00
Dániel Szabó
b27ac0831f
Merge pull request #141 from DhruvaSambrani/master
Update header.html to fix extra space
2023-03-26 22:27:14 +03:00
Dániel Szabó
86f18f34d2
Merge pull request #128 from Tyfui90/defect/issue-127-default-expiry-option-never-unselected
default expiry of never selected in dropdown when passed as env var
2023-03-26 22:14:05 +03:00
Daniel Szabo
1c3493202f revert option grouping 2023-03-26 22:13:10 +03:00
Dániel Szabó
18e056106b
Merge pull request #146 from rissson/fix-create-no-expiration-data
Fix expiration on create if it's not specified and no_eternal_pasta is set
2023-03-26 22:01:51 +03:00
Dániel Szabó
ded125c89b
Merge pull request #143 from 7a6163/bugfix/xss
bugfix: stored XSS
2023-03-26 21:44:35 +03:00
Marc 'risson' Schmitt
d6f5c0ca52
create: refactor expiry conversion to timestamp
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2023-02-24 23:31:50 +01:00
Marc 'risson' Schmitt
2f0ee11cae
Fix expiration on create if it's not specified and no_eternal_pasta is set
If no_eternal_pasta is specified, when calling /upload with no
expiration, the expiration currently defaults at 0. This change
makes it default to the maximal expiry.

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2023-02-24 23:07:29 +01:00
Zac
6907bb4f13 bugfix: stored XSS
bugfix: Copy Text is not working when the button Copy Url does not exist.
2023-02-21 09:52:00 +08:00
Dhruva Sambrani
d8d05637db
Update header.html 2023-02-06 13:25:57 +05:30
tyfui90
5773c36699 default expiry of never selected in dropdown when passed as env var 2022-12-05 16:33:33 -08:00
Joshua Chia
04035269f3 Follow-up to 7522d419
In addition to the case of creating, #88 also affects the case of editing. This commit fixes editing.
2022-11-18 11:32:35 +08:00
Daniel Szabo
84136f1106 Merge branch 'master' of https://github.com/szabodanika/microbin 2022-11-14 21:43:14 +02:00
Daniel Szabo
ba784da74e Merge branch 'pr/95' 2022-11-14 21:42:35 +02:00
Dániel Szabó
0a80ac1359
Merge pull request #101 from albocc/feature/Add_3_days_expiration_option
Added expiration option for 3 days
2022-11-14 21:30:34 +02:00
Dániel Szabó
6564499c98
Merge pull request #104 from alex3305/cleanup-readme
Clean up README
2022-11-14 21:28:21 +02:00
Dániel Szabó
4fa2cc2a53
Merge branch 'master' into cleanup-readme 2022-11-14 21:28:12 +02:00
Dániel Szabó
4279653ca9
Update README.MD 2022-11-14 21:24:08 +02:00
Alex van den Hoogen
53326b0435
Clean up README
* microbin.eu reference was not working
* Consistent MicroBin spelling
* Link to LICENSE
* Clean up whitespace
* Insert codeblocks
* Set heading 2 instead of heading 3 (as h1 was already used by the title, next in line would be h2)
* Set monospace for some references
2022-11-09 19:55:13 +01:00
figsoda
68f4081745 fix typos 2022-11-09 10:46:51 -05:00
figsoda
089bb95c4f cargo fmt 2022-11-09 10:45:53 -05:00
figsoda
5f05206891
Merge pull request #1 from kwiniarski97/patch-1
doc: fixed invalid link to main website in readme
2022-11-09 10:45:32 -05:00
albocc
4fcd4e9e19 Added expiration option for 3 days 2022-11-09 11:57:49 +01:00
Karol Winiarski
89f902f99f
doc: fixed invalid link to main website
Link to a resource outside the repo itself should be prepended with https.
2022-11-09 10:26:44 +01:00
figsoda
958466818b apply clippy suggestions 2022-11-08 16:30:16 -05:00
figsoda
f41c2eb66b rename README.MD to README.md 2022-11-08 16:22:42 -05:00
Daniel Szabo
76cfc906ef Update docker.yml
Fix out of memory error on GH Actions build and automate GitHub Release with artifacts ( #47). See https://github.com/docker/build-push-action/issues/654#issuecomment-1285190151 and https://github.com/docker/build-push-action/issues/621
2022-11-08 11:46:11 +02:00
Daniel Szabo
66f6e0e46f Update docker.yml
Fix out of memory error on GH Actions build and automate GitHub Release with artifacts ( #47). See https://github.com/docker/build-push-action/issues/654#issuecomment-1285190151 and https://github.com/docker/build-push-action/issues/621
2022-11-08 00:23:17 +02:00
Dániel Szabó
4362d934e3
Merge pull request #85 from szabodanika/szabodanika
Merge personal branch to master
2022-11-07 20:34:14 +02:00
Daniel Szabo
d7f0f9637d Update build date - v1.2.0 is ready 2022-11-07 20:33:33 +02:00
Daniel Szabo
edd46eae58 Improved /pasta and /pastalist UX
- adjusted table column widths so buttons dont wrap into next row
- added Open button to url pastas so clicking on their keys redirect to their /pasta view, consistent with the rest of the pastas
- added copy redirect button to /pasta view of url pastas to make it easier to get the /url endpoint url
2022-11-07 20:30:33 +02:00
Daniel Szabo
c6e2b026e6 Improved QR code view
- added backlink to pasta page
- added link on QR SVG to its destination
- 404 if incorrect id
- QR code of URL pasta will now redirect to /url endpoint
2022-11-07 20:28:45 +02:00
Daniel Szabo
5854572e87 Reduced QR code size to fit phone screens 2022-11-07 20:26:44 +02:00
Daniel Szabo
5e1fcff979 Fixed a bug that caused url pastas to become plain text pastas 2022-11-07 20:25:42 +02:00
Daniel Szabo
ca1cd91635 Removed logo from README.MD
Because GitHub does not render HTML in Markdown files (correctly anyway)
2022-11-07 17:59:47 +02:00
Daniel Szabo
dac9ae8385 Moved FUNDING.yml out of workflows folder 2022-11-07 17:58:03 +02:00
Daniel Szabo
719691dfac Update README.MD
- Moved documentation to website
- Updated screenshots
- Added logo
- Reorganised feature list
- Added licence at the bottom
- Moved screenshot to .git folder
2022-11-07 17:57:50 +02:00
Daniel Szabo
b9a6717ba0 Add drag-and-drop to new file attachment button 2022-11-07 00:30:17 +02:00
Daniel Szabo
5e2513eb1f Fix empty pasta content showing code field
content has to be "No Text Content" to hide the code field on the pasta view
2022-11-07 00:27:25 +02:00
Daniel Szabo
7522d41919 Fix #88
Apparently if a multipart field contains multiple dashes, it will be read in multiple chunks and these chunks should be concatenated in order to read the entire field correctly.
2022-11-06 23:47:56 +02:00
Daniel Szabo
c6e5c6f018 Merge branch 'master' into szabodanika 2022-11-06 23:21:45 +02:00
Dániel Szabó
6a0c6b736d
Merge pull request #86 from henry40408/feature/issue-54
hashids
2022-11-06 22:58:46 +02:00
Daniel Szabo
2198cbdff9 Getting ready for 1.2 & new site release
- improved support for serving static resources from the binary, now supporting images
- added new logo
- changed save button
- fixed footer attribution text, it is not true anymore that MicroBin is made by myself
- replaced footer GitHub link with microbin.eu link
2022-11-01 20:56:07 +02:00
Heng-Yi Wu
b5da40fbdc
feat: hashids 2022-11-01 21:19:54 +08:00
Daniel Szabo
44b55ae08e Getting ready for 1.2.0 release: many smaller requests implemented
- Implements #7
- Implements #42 and therefore #64
- Improved #53
- Implements #59
- Implements #61
- Implements #63
- Implements #80
- Implements #84
- Added Info page
- Removed Help page
- Bumped version number to 1.2.0
- Fixed a bug where wide mode was still 720px wide
- Created FUNDING.yml
- Reorganised arguments in README.MD and documented new options
- Updated SECURITY.MD
- Added display of last read time and read count
- Increased default width to 800px to make UI less cluttered
- Reorganised index page
- New, better attach file button

I want to spend some time testing these changes and let everyone have a look at them before tagging and releasing new artifacts.
2022-10-29 14:11:55 +03:00
Daniel Szabo
769901c895 Added copy button for URL redirects
Fixes #80
2022-10-27 17:23:39 +03:00
Dániel Szabó
d2e7234d96 small ui improvements
- fix width for pure html mode
- improve copy button look and placement
- make input field heights more consistent on pasta creation page
2022-10-27 14:12:11 +03:00
Daniel Szabo
e258bcc2bd
added missing bracket in args.rs 2022-10-25 12:51:59 +03:00
Dániel Szabó
dc2c7094a8
Merge pull request #78 from HeapUnderfl0w/public-path
Public Url for Paths
2022-10-25 12:24:33 +03:00
HeapUnderflow
43061699f5
Merge branch 'master' into public-path 2022-10-24 12:57:21 +02:00
HeapUnderflow
4980d72df2
Add example to README.MD 2022-10-24 12:55:09 +02:00
Dániel Szabó
2e981b3128
Merge pull request #68 from hay-kot/chores/gitignore
Add gitignore
2022-10-22 21:46:37 +03:00
Dániel Szabó
3e58ba325a
Merge pull request #70 from hay-kot/feat/implement-copy-button
feat: add copy button to viewer
2022-10-22 21:42:51 +03:00
Hayden
b1ccb43855 remove hash 2022-10-22 10:37:43 -08:00
Hayden
fd8a66bcbc use proper semantics for a tag as button 2022-10-22 10:34:39 -08:00
Hayden
e031ea0e95 use a tag instead of button 2022-10-22 10:30:16 -08:00
Dániel Szabó
ec4d764f5a
Merge pull request #71 from hay-kot/feat/add-site-favicon
feat: add site favicon
2022-10-22 21:15:06 +03:00
Hayden
e8e21d561e remove icon and change styles 2022-10-22 10:12:15 -08:00
Hayden
487de0fcf7 use gray favicon 2022-10-22 10:02:52 -08:00
Hayden
7f4f784f2b add pasta_data 2022-10-22 09:55:11 -08:00
Dániel Szabó
52d87652ea
Merge pull request #69 from hay-kot/docker/expose-port
Expose port 8080 in dockerfile
2022-10-22 20:49:44 +03:00
Dániel Szabó
bc26fe87a5
Merge pull request #72 from hay-kot/feat/disable-file-attachment
feat: disable file attachment
2022-10-22 20:39:30 +03:00
Dániel Szabó
4926c578ec
Merge pull request #73 from stavros-k/patch-1
Fix --footer-text reference
2022-10-22 19:39:28 +03:00
HeapUnderflow
cfd494f80d
Do not append a second slash when no public path is set 2022-10-12 17:42:42 +02:00
HeapUnderflow
a404f9a997
Allow setting a public url for all paths 2022-10-12 17:13:21 +02:00
Stavros Kois
e786fa5a22
Fix --footer-text reference 2022-10-03 00:36:29 +03:00
Hayden
c39b778234 properly escape content 2022-10-01 20:50:05 -08:00
Hayden
0b5dea5dd1 add flag to docs 2022-09-30 22:12:24 -08:00
Hayden
82c30ce6cd conditionally render file upload 2022-09-30 22:11:25 -08:00
Hayden
fc3998243b ignore file processing if no_file_upload is true 2022-09-30 22:11:03 -08:00
Hayden
e17b26994f add cli args to disable file upload 2022-09-30 22:10:43 -08:00
Hayden
ef5d07392b add in template and as static resource 2022-09-30 21:52:08 -08:00
Hayden
cc504f781e add favicon resource 2022-09-30 21:51:52 -08:00
Hayden
1e8b17bb89 add copy button to viewer 2022-09-30 21:37:26 -08:00
Hayden
8223dd4973 export port 8080 in dockerfile 2022-09-30 21:11:16 -08:00
Hayden
6cea6262b8 init git ignore 2022-09-30 21:08:02 -08:00
Dániel Szabó
3ca89291dc
Merge pull request #57 from frdmn/patch-1
Fix Docker repository link in README to allow non-authenticated Docker Hub users.
2022-09-27 21:23:24 +03:00
Jonas Friedmann
2322c6713e
Fix Docker repository link 2022-09-27 20:09:44 +02:00
Dániel Szabó
05ad1d46c1
Update README.MD
Updated docker volume path according to 08871e1. Fixes #55
2022-09-27 16:19:41 +00:00
Dániel Szabó
d36472bcac
Update README.MD 2022-09-26 12:10:01 +00:00
Dániel Szabó
ff28faa222
Update README.MD 2022-09-26 11:53:54 +00:00
Dániel Szabó
60aac1aea7
Merge pull request #49 from arghyadipchak/master
Automated docker build for DockerHub + updated cargo.toml
2022-09-26 10:48:40 +03:00
Dániel Szabó
afa3c516ee
Merge branch 'master' into master 2022-09-26 10:35:38 +03:00
Dániel Szabó
7cd136cd7b
Merge pull request #50 from techiall/fix/dockerfile
Replace base docker image with Bitnami Minideb to fix #50 and #39
2022-09-16 19:37:57 +03:00
techial
a593ea0160
Update runing image 2022-08-28 16:27:38 +08:00
techial
5ae641fda0 Update Dockerfile 2022-08-26 22:25:59 +08:00
Arghyadip Chakraborty
08871e15b6 Automated docker build 2022-08-25 22:25:03 +05:30
Dániel Szabó
f55a5eba96
Merge pull request #43 from FoxFromDarkness66/patch-3
Update AUR installation method
2022-08-06 22:55:15 +01:00
FixFromDarkness
011cc25490
Update AUR installation method 2022-08-05 12:25:05 +03:00
Dániel Szabó
d44a3081bc
Delete FUNDING.yml 2022-08-02 22:36:06 +01:00
Dániel Szabó
51f7f54be7
Create SECURITY.md 2022-08-01 10:07:42 +01:00
Daniel Szabo
a3fc97a460 bump version number 2022-07-31 22:06:41 +01:00
Daniel Szabo
05941f0d6f Isolate pasta uploads from database.json by moving them into pasta_data/public/ 2022-07-31 21:49:36 +01:00
Daniel Szabo
7b4cd7c26e Implement upload filename sanitisation 2022-07-31 21:31:35 +01:00
Daniel Szabo
f54d5bd780 Add pasta ID + pasta URL to pasta page. Fix #36 2022-07-31 19:41:19 +01:00
Daniel Szabo
435c07d75e Implement manual deletion behaviour and fix #35 2022-07-31 19:18:07 +01:00
Dániel Szabó
cc09d1b529
Merge pull request #38 from uniqueNullptr2/uniqueNullptr2-fix-title-in-template
fix usage of title arg in template
2022-07-31 18:59:37 +01:00
uniqueNullptr2
60c3a1f9ac fix usage of title arg in template 2022-07-25 15:45:07 +02:00
Dániel Szabó
d4d94b61da
Merge pull request #34 from uniqueNullptr2/uniquenullptr2-add-config-from-env
add configuration from env to all clap options
2022-07-25 14:04:26 +01:00
Dániel Szabó
9053211904
Merge pull request #31 from dvdsk/file-size
Adds file size to pasta with an attachment
2022-07-25 13:39:00 +01:00
uniqueNullptr2
35a512680c fix mistype of syntax highlight option 2022-07-20 09:13:31 +02:00
uniqueNullptr2
bcd620ed43 add configuration from env to all clap options 2022-07-20 08:50:23 +02:00
Dániel Szabó
556f4e87df
Merge pull request #33 from amnesiacsardine/patch-1
Update to README.MD regarding Dockerfile
2022-07-18 00:05:56 +01:00
amnesiacsardine
fa88bce917
Update README.MD
Added how to pass command line arguments in the Dockerfile
2022-07-16 13:22:31 +02:00
Dániel Szabó
a5d326b679
Merge pull request #30 from dvdsk/master
Fixes #29 and displays pasta list in local timezone
2022-07-15 22:51:26 +01:00
Dániel Szabó
465873e095
Merge pull request #28 from FoxFromDarkness66/patch-2
Add bind address CL option
2022-07-15 22:47:39 +01:00
dvdsk
39233e9447
fixes #6 adding the size of the attached file 2022-07-14 01:08:13 +02:00
dvdsk
738e036cb5
pasta times are in systems local timezone 2022-07-13 23:55:28 +02:00
dvdsk
de2cc48f88
fixes #29 (time formating) 2022-07-13 23:54:48 +02:00
Dániel Szabó
0687f44137
Merge pull request #27 from FoxFromDarkness66/patch-1
Add AUR installation method
2022-07-07 23:12:48 +01:00
FixFromDarkness
fec933c5ec * Revert default address to 0.0.0.0 due to docker usage & compatibility
* Add --bind option to readme & change some examples
2022-07-07 20:08:29 +03:00
FixFromDarkness
cc2dd1e1fe * Add --bind option
* Changed default bind address to localhost
* Fix wrong log text about binding address
2022-07-07 19:45:31 +03:00
FixFromDarkness
85ed1b2b92
Add AUR installation method
AUR is Arch User Repository, "community-driven repository for Arch users. It contains package descriptions (PKGBUILDs) that allow you to compile a package from source with makepkg and then install it via pacman." 
I've made an AUR package that will allow any arch-based distro user to install and update microban to the latest version without manual version cheking&compiling. It will be easier for them to find it if you add information about this in the readme
2022-07-07 19:15:26 +03:00
Dániel Szabó
73ec59ccda
Merge pull request #21 from Arizard/master
Updates Dockerfile and adds docker build instructions
2022-06-27 20:58:01 +01:00
Arie
f5b9036a2a Uses volume absolute path, changes wording 2022-06-27 10:52:37 +10:00
Arie
0d43f2f60a Updates README with docker build instructions 2022-06-27 10:39:32 +10:00
Arie
aa9246da4e Removes COPY instruction for static directory 2022-06-27 10:37:25 +10:00
Dániel Szabó
1c21943c75
Merge pull request #17 from dvdsk/patch-1
Remove --linenumbers in systemd example
2022-06-05 19:30:05 +01:00
David Kleingeld
7f4b9f4aee
Remove --linenumbers in systemd example 2022-06-05 19:23:28 +02:00
Dániel Szabó
acd547dbf3
Update FUNDING.yml 2022-06-05 01:41:09 +01:00
Daniel Szabo
ce8bd4dd02 Added cargo information and readme badges 2022-06-05 00:20:23 +01:00
Dániel Szabó
81bf17e004
Merge pull request #16 from szabodanika/add_render.yaml
Add render.com deployment support
2022-06-05 00:02:44 +01:00
Dániel Szabó
92a29a02e5
Update render.yaml 2022-06-04 23:30:39 +01:00
Daniel Szabo
8b1702365c Add "Deploy to Render" button to README.MD 2022-06-04 23:05:47 +01:00
Daniel Szabo
291438e771 Update render.yaml 2022-06-04 22:55:16 +01:00
Daniel Szabo
497a8ef0e3 Add render.yaml 2022-06-04 22:51:09 +01:00
Daniel Szabo
ff921dc103 Additional changes to prev. commit 2022-06-04 22:21:22 +01:00
Daniel Szabo
fe013d9d85 Merge branch 'master' of https://github.com/szabodanika/microbin 2022-06-04 21:52:27 +01:00
Daniel Szabo
be3ac27920 Quick patch:
- Changed 302 responses to 200 where needed
- Fixed a bug where expiring pastas cause MicroBin to crahs
- Fixed a bug where water.css didn't have the correct MIME type
- Fixed a bug where missing doctype declaration caused styling issues in Firefox
2022-06-04 21:50:34 +01:00
Dániel Szabó
2f13a0e8e7
Create FUNDING.yml 2022-06-04 17:03:14 +01:00
Daniel Szabo
35d4df2cb8 Fixed a bug where opening private pastas would crash MicroBin 2022-06-03 18:11:14 +01:00
Daniel Szabo
4cc737731a Multiple enhancements and bugfixes
!Breaking change! - The updated version will not be able to read your old database file

Major improvements:
- Added editable pastas
- Added private pastas
- Added line numbers
- Added support for wide mode (1080p instead of 720p)
- Added syntax highlighting support
- Added read-only mode
- Added built-in help page
- Added option to remove logo, change title and footer text

Minor improvements:
- Improved looks in pure html mode
- Removed link to GitHub repo from navbar
- Broke up 7km long main.rs file into smaller modules
- Moved water.css into a template instead of serving it as an external resource
- Made Save button a bit bigger
- Updated README.MD

Bugfixes:
- Fixed a bug where an incorrect animal ID in a request would cause a crash
- Fixed a bug where an empty or corrupt JSON database would cause a crash
2022-06-03 17:24:34 +01:00
76 changed files with 15066 additions and 1116 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"]

237
.env Normal file
View file

@ -0,0 +1,237 @@
# Require username for HTTP Basic Authentication when
# visiting the service. If basic auth username is set but
# basic auth password is not, just leave the password field
# empty when logging in. You can also just go to
# https://username:password@yourserver.net or
# https://username@yourserver.net if password is not set
# instead of typing into the password
# Default value: unset
# export MICROBIN_BASIC_AUTH_USERNAME=
# Require password for HTTP Basic Authentication when
# visiting the service. Will not have any affect unless
# basic auth username is also set. If basic auth username is
# set but basic auth password is not, just leave the
# password field empty when logging in. You can also just go
# to https://username:password@yourserver.net or
# https://username@yourserver.net if password is not set
# instead of typing into the password prompt.
# Default value: unset
# export MICROBIN_BASIC_AUTH_PASSWORD=
# Enables administrator interface at yourserver.com/admin/
# if set, disables it if unset. If admin username is set but
# admin password is not, just leave the password field empty
# when logging in.
# Default value: admin
export MICROBIN_ADMIN_USERNAME=admin
# Enables administrator interface at yourserver.com/admin/
# if set, disables it if unset. Will not have any affect
# unless admin username is also set. If admin username is
# set but admin password is not, just leave the password
# field empty when logging in.
# Default value: m1cr0b1n
export MICROBIN_ADMIN_PASSWORD=m1cr0b1n
# Enables editable pastas. You will still be able to make
# 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: 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: false
export MICROBIN_HIDE_HEADER=false
# Hides the footer on every page.
# Default value: false
export MICROBIN_HIDE_FOOTER=false
# Hides the MicroBin logo from the navigation bar on every
# page.
# Default value: false
export MICROBIN_HIDE_LOGO=false
# Disables the /pastalist endpoint, essentially making all
# pastas private.
# Default value: false
export MICROBIN_NO_LISTING=false
# Enables syntax highlighting support. When creating a new
# pasta, a new dropdown selector will be added where you can
# select your pasta's syntax, or just leave it empty for no
# highlighting.
export MICROBIN_HIGHLIGHTSYNTAX=true
# Sets the port for the server will be listening on.
# Default value: 8080
export MICROBIN_PORT=8080
# Sets the bind address for the server will be listening on.
# Both ipv4 and ipv6 are supported. Default value: "0.0.0.0".
# Example value: "myserver.net", "127.0.0.1".
export MICROBIN_BIND="0.0.0.0"
# Enables private pastas. Adds a new checkbox to make your
# 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: 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.
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
export MICROBIN_JSON_DB=false
# Add the given public path prefix to all urls. This allows
# you to host MicroBin behind a reverse proxy on a subpath.
# Note that MicroBin itself still expects all routes to be
# as without this option, and thus is unsuited if you are
# running MicroBin directly. Default value: unset. Example
# values: https://myserver.com/ or https://192.168.0.10:8080/
# export MICROBIN_PUBLIC_PATH=
# Sets a shortened path to use when the user copies URL from
# the application. This will also use shorter endpoints,
# such as /p/ instead if /pasta/. Default value:
# unset.Example value: https://b.in/ export
# MICROBIN_SHORT_PATH=
# 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
# Enables showing read count on pasta pages.
# Default value: false
export MICROBIN_SHOW_READ_STATS=true
# Adds your title of choice to the
# navigation bar.
# Default value: unset
# export MICROBIN_TITLE=
# Number of workers MicroBin is allowed to have. Increase
# this to the number of CPU cores you have if you want to go
# beast mode, but for personal use one worker is enough.
# Default value: 1.
export MICROBIN_THREADS=1
# Sets the garbage collector time limit. Pastas not accessed
# for N days are removed even if they are set to never
# expire.
# Default value: 90.
# To turn off GC: 0.
export MICROBIN_GC_DAYS=90
# Enables or disables the "Burn after" function
# Default value: false
export MICROBIN_ENABLE_BURN_AFTER=true
# Sets the default burn after setting on the main screen.
# Default value: 0. Available expiration options: 1, 10,
# 100, 1000, 10000, 0 (= no limit)
export MICROBIN_DEFAULT_BURN_AFTER=0
# Changes the maximum width of the UI from 720 pixels to
# 1080 pixels.
# Default value: false
export MICROBIN_WIDE=false
# Enables generating QR codes for pastas. Requires
# the public path to also be set.
# Default value: false
export MICROBIN_QR=true
# Toggles "Never" expiry settings for pastas. Default
# value: false
export MICROBIN_ETERNAL_PASTA=false
# Enables "Read-only" uploads. These are unlisted and
# unencrypted, but can be viewed without password if you
# have the URL. Editing and removing requires password.
# Default value: true
export MICROBIN_ENABLE_READONLY=true
# Sets the default expiry time setting on the main screen.
# Default value: 24hour Available expiration options: 1min,
# 10min, 1hour, 24hour, 1week, never
export MICROBIN_DEFAULT_EXPIRY=24hour
# Disables and hides the file upload option in the UI.
# Default value: false
export MICROBIN_NO_FILE_UPLOAD=false
# Replaced the built-in water.css stylesheet with the URL
# you provide. Default value: unset. Example value:
# https://myserver.net/public/mystyle.css
# export MICROBIN_CUSTOM_CSS=
# Use short hash strings in the URLs instead of animal names
# to make URLs shorter. Does not change the underlying data
# stored, just how pastas are recalled.
# Default value: false
export MICROBIN_HASH_IDS=false
# Enables server-side encryption. This will add private
# privacy level, where the user sends plain unencrypted data
# (still secure, because you use HTTPS, right?), but the
# server sees everything that the user submits, therefore
# the user does not have complete and absolute protection.
# Default value: false
export MICROBIN_ENCRYPTION_CLIENT_SIDE=true
# Enables client-side encryption. This will add the secret
# privacy level where the user's browser encrypts all data
# with JavaScript before sending it over to MicroBin, which
# encrypt the data once again on server side.
# Default value: false
export MICROBIN_ENCRYPTION_SERVER_SIDE=true
# Limit the maximum file size users can upload without
# encryption. Default value: 256.
export MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB=256
# Limit the maximum file size users can upload with
# 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
# 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

4
.github/FUNDING.yml vendored Normal file
View file

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

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.

BIN
.github/index.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
.github/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

196
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,196 @@
name: GitHub and Docker Release
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
release:
name: Publish to Github Relases
outputs:
rc: ${{ steps.check-tag.outputs.rc }}
strategy:
matrix:
include:
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: aarch64-apple-darwin
os: macos-latest
use-cross: true
cargo-flags: ""
- target: aarch64-pc-windows-msvc
os: windows-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: x86_64-apple-darwin
os: macos-latest
cargo-flags: ""
- target: x86_64-pc-windows-msvc
os: windows-latest
cargo-flags: ""
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: i686-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: i686-pc-windows-msvc
os: windows-latest
use-cross: true
cargo-flags: ""
- target: armv7-unknown-linux-musleabihf
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: arm-unknown-linux-musleabihf
os: ubuntu-latest
use-cross: true
cargo-flags: ""
- target: mips-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mipsel-unknown-linux-musl
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
- target: mips64el-unknown-linux-gnuabi64
os: ubuntu-latest
use-cross: true
cargo-flags: "--no-default-features"
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v2
- name: Check Tag
id: check-tag
shell: bash
run: |
tag=${GITHUB_REF##*/}
echo "::set-output name=version::$tag"
if [[ "$tag" =~ [0-9]+.[0-9]+.[0-9]+$ ]]; then
echo "::set-output name=rc::false"
else
echo "::set-output name=rc::true"
fi
- name: Install Rust Toolchain Components
uses: actions-rs/toolchain@v1
with:
override: true
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
run: |
gcc --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- name: Build
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.use-cross }}
command: build
args: --locked --release --target=${{ matrix.target }} ${{ matrix.cargo-flags }}
- name: Build Archive
shell: bash
id: package
env:
target: ${{ matrix.target }}
version: ${{ steps.check-tag.outputs.version }}
run: |
set -euxo pipefail
bin=${GITHUB_REPOSITORY##*/}
src=`pwd`
dist=$src/dist
name=$bin-$version-$target
executable=target/$target/release/$bin
if [[ "$RUNNER_OS" == "Windows" ]]; then
executable=$executable.exe
fi
mkdir $dist
cp $executable $dist
cd $dist
if [[ "$RUNNER_OS" == "Windows" ]]; then
archive=$dist/$name.zip
7z a $archive *
echo "::set-output name=archive::`pwd -W`/$name.zip"
else
archive=$dist/$name.tar.gz
tar czf $archive *
echo "::set-output name=archive::$archive"
fi
- name: Publish Archive
uses: softprops/action-gh-release@v0.1.15
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: false
files: ${{ steps.package.outputs.archive }}
prerelease: ${{ steps.check-tag.outputs.rc == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
name: Publish to Docker Hub
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: release
steps:
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ secrets.DOCKERHUB_REPO }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
build-args: |
REPO=${{ github.repository }}
VER=${{ github.ref_name }}
platforms: |
linux/amd64
linux/arm64
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

15
.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
pasta_data/*
microbin_data/*
*.env
**/**/microbin-data

3708
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,71 @@
[package]
name="microbin"
version="0.2.0"
edition="2021"
name = "microbin"
version = "2.0.4"
edition = "2021"
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", "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"
askama="0.10"
askama-filters={ version = "0.1.3", features = ["chrono"] }
chrono="0.4.19"
rand="0.8.5"
linkify="0.8.1"
clap={ version = "3.1.12", features = ["derive"] }
actix-multipart = "0.4.0"
futures = "0.3"
sanitize-filename = "0.3.0"
log = "0.4"
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"
clap = { version = "3.1.12", features = ["derive", "env"] }
env_logger = "0.9.0"
actix-web-httpauth = "0.6.0"
futures = "0.3"
harsh = "0.2"
html-escape = "0.2.13"
lazy_static = "1.4.0"
rutie = "0.8.4"
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
strip = true

View file

@ -1,31 +1,41 @@
# latest rust will be used to build the binary
FROM rust:latest as builder
FROM rust:latest as build
# the temporary directory where we build
WORKDIR /usr/src/microbin
WORKDIR /app
RUN \
DEBIAN_FRONTEND=noninteractive \
apt-get update &&\
apt-get -y install ca-certificates tzdata
# copy sources to /usr/src/microbin on the temporary container
COPY . .
# run release build
RUN cargo build --release
# create final container using slim version of debian
FROM debian:buster-slim
# microbin will be in /usr/local/bin/microbin/
WORKDIR /usr/local/bin
# copy built exacutable
COPY --from=builder /usr/src/microbin/target/release/microbin /usr/local/bin/microbin
# copy /static folder containing the stylesheets
COPY --from=builder /usr/src/microbin/static /usr/local/bin/static
# Install Ruby (no need if you're disabling all plugins)
RUN \
apt-get update && \
apt-get install -y ruby
CARGO_NET_GIT_FETCH_WITH_CLI=true \
cargo build --release
# run the binary
CMD ["microbin"]
# https://hub.docker.com/r/bitnami/minideb
FROM bitnami/minideb:latest
# microbin will be in /app
WORKDIR /app
RUN mkdir -p /usr/share/zoneinfo
# copy time zone info
COPY --from=build \
/usr/share/zoneinfo \
/usr/share/
COPY --from=build \
/etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/ca-certificates.crt
# copy built executable
COPY --from=build \
/app/target/release/microbin \
/usr/bin/microbin
# Expose webport used for the webserver to the docker runtime
EXPOSE 8080
ENTRYPOINT ["microbin"]

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

@ -1,66 +0,0 @@
# MicroBin
![Screenshot](git/index.png)
MicroBin is a super tiny and simple self hosted pastebin app written in Rust. The executable is around 6MB and it uses 2MB memory (plus your pastas, because they are all stored in the memory at the moment).
### Features
- Is very small
- File uploads
- Raw pasta content (/raw/[animals])
- URL shortening and redirection
- Automatic dark mode (follows system preferences)
- Very simple database (json + files) for portability and easy backups
- Animal names instead of random numbers for pasta identifiers (64 animals)
- Automatically expiring pastas
- Never expiring pastas
- Listing and manually removing pastas (/pastalist)
- Very little CSS and absolutely no JS (see [water.css](https://github.com/kognise/water.css))
### Installation
Simply clone the repository, build it with `cargo build --release` and run the `microbin` executable in the created `target/release/` directory. It will start on port 8080. You can change the port with `-p` or `--port` CL arguments. For other arguments see [the Wiki](https://github.com/szabodanika/microbin/wiki).
To install it as a service on your Linux machine, create a file called `/etc/systemd/system/microbin.service`, paste this into it with the `[username]` and `[path to installation directory]` replaced with the actual values.
```
[Unit]
Description=MicroBin
After=network.target
[Service]
Type=simple
Restart=always
User=[username]
RootDirectory=/
WorkingDirectory=[path to installation directory]
ExecStart=[path to installation directory]/target/release/microbin
[Install]
WantedBy=multi-user.target
```
Then start the service with `systemctl start microbin` and enable it on boot with `systemctl enable microbin`.
### Create Pasta with cURL
Simple text Pasta: `curl -d "expiration=10min&content=This is a test pasta" -X POST https://microbin.myserver.com/create`
File contents: `curl -d "expiration=10min&content=$( < mypastafile.txt )" -X POST https://microbin.myserver.com/create`
Available expiration options:
`1min`, `10min`, `1hour`, `24hour`, `1week`, `never`
Use cURL to read the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow`,
or to download the pasta: `curl https://microbin.myserver.com/rawpasta/fish-pony-crow > output.txt` (use /file instead of /rawpasta to download attached file).
### Needed improvements
- ~~Persisting pastas on disk (currently they are lost on restart)~~ (added on 2 May 2022)
- ~~Configuration with command line arguments (ports, enable-disable pasta list, footer, etc)~~ (added on 7 May 2022)
- ~~File uploads~~ (added on 2 May 2022)
- ~~URL shortening~~ (added on 23 April 2022)
- Removing pasta after N reads
- CLI tool (beyond wget)
- Better instructions and documentation - on GitHub and built in

77
README.md Normal file
View file

@ -0,0 +1,77 @@
![Screenshot](.github/index.png)
# MicroBin
![Build](https://github.com/szabodanika/microbin/actions/workflows/rust.yml/badge.svg)
[![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)
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)!
### Or host MicroBin yourself
Run our quick docker setup script ([DockerHub](https://hub.docker.com/r/danielszabo99/microbin)):
```bash
bash <(curl -s https://microbin.eu/docker.sh)
```
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;
source .env;
microbin
```
On our website [microbin.eu](https://microbin.eu), you will find the following:
- [Screenshots](https://microbin.eu/screenshots/)
- [Guide and Documentation](https://microbin.eu/docs/intro)
- [Donations and Sponsorships](https://microbin.eu/sponsorship)
- [Roadmap](https://microbin.eu/roadmap)
## Features
- Entirely self-contained executable, MicroBin is a single file!
- Server-side and client-side encryption
- File uploads (e.g. `server.com/file/pig-dog-cat`)
- Raw text serving (e.g. `server.com/raw/pig-dog-cat`)
- QR code support
- 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))
- 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, 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:
- To send long texts to other people,
- To send large files to other people,
- 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,
- 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-2024

12
SECURITY.md Normal file
View file

@ -0,0 +1,12 @@
# Security Policy
## Version Support
Currently we only have capacity to support the latest version of MicroBin. We recommend that you always update to the newest one and check our pages regularly for announcements.
## Reporting a Vulnerability
Security vulnerabilities can be reported directly to the developer/maintainer at d@szab.eu.
Sensitive information may be GPG encrypted with my public key available at
https://szab.eu/assets/files/daniel-szabo-pub.asc.

47
compose.yaml Normal file
View file

@ -0,0 +1,47 @@
services:
microbin:
image: danielszabo99/microbin:latest
restart: always
ports:
- "${MICROBIN_PORT}:8080"
volumes:
- ./microbin-data:/app/microbin_data
environment:
MICROBIN_BASIC_AUTH_USERNAME: ${MICROBIN_BASIC_AUTH_USERNAME}
MICROBIN_BASIC_AUTH_PASSWORD: ${MICROBIN_BASIC_AUTH_PASSWORD}
MICROBIN_ADMIN_USERNAME: ${MICROBIN_ADMIN_USERNAME}
MICROBIN_ADMIN_PASSWORD: ${MICROBIN_ADMIN_PASSWORD}
MICROBIN_EDITABLE: ${MICROBIN_EDITABLE}
MICROBIN_FOOTER_TEXT: ${MICROBIN_FOOTER_TEXT}
MICROBIN_HIDE_FOOTER: ${MICROBIN_HIDE_FOOTER}
MICROBIN_HIDE_HEADER: ${MICROBIN_HIDE_HEADER}
MICROBIN_HIDE_LOGO: ${MICROBIN_HIDE_LOGO}
MICROBIN_NO_LISTING: ${MICROBIN_NO_LISTING}
MICROBIN_HIGHLIGHTSYNTAX: ${MICROBIN_HIGHLIGHTSYNTAX}
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}
MICROBIN_GC_DAYS: ${MICROBIN_GC_DAYS}
MICROBIN_ENABLE_BURN_AFTER: ${MICROBIN_ENABLE_BURN_AFTER}
MICROBIN_DEFAULT_BURN_AFTER: ${MICROBIN_DEFAULT_BURN_AFTER}
MICROBIN_WIDE: ${MICROBIN_WIDE}
MICROBIN_QR: ${MICROBIN_QR}
MICROBIN_ETERNAL_PASTA: ${MICROBIN_ETERNAL_PASTA}
MICROBIN_ENABLE_READONLY: ${MICROBIN_ENABLE_READONLY}
MICROBIN_DEFAULT_EXPIRY: ${MICROBIN_DEFAULT_EXPIRY}
MICROBIN_NO_FILE_UPLOAD: ${MICROBIN_NO_FILE_UPLOAD}
MICROBIN_CUSTOM_CSS: ${MICROBIN_CUSTOM_CSS}
MICROBIN_HASH_IDS: ${MICROBIN_HASH_IDS}
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}

35
docker-setup.sh Executable file
View file

@ -0,0 +1,35 @@
#!/bin/bash
# Check if wget is installed; if not, try to use curl
if ! command -v wget &> /dev/null
then
download_command="curl -O"
else
download_command="wget"
fi
# Get installation directory from user
echo -e "\033[1mEnter installation directory (default is /usr/share/microbin):\033[0m"
read install_dir
install_dir=${install_dir:-/usr/share/microbin}
# Create directory and download files
mkdir -p $install_dir
cd $install_dir
$download_command https://raw.githubusercontent.com/szabodanika/microbin/master/.env
$download_command https://raw.githubusercontent.com/szabodanika/microbin/master/compose.yaml
# Get public path URL and port from user
echo -e "\033[1mEnter public path URL (e.g. https://microbin.myserver.net or http://localhost:8080):\033[0m"
read public_path
echo -e "\033[1mEnter port number (default is 8080):\033[0m"
read port
port=${port:-8080}
# Update environment variables in .env file
sed -i "s|MICROBIN_PUBLIC_PATH=.*|MICROBIN_PUBLIC_PATH=${public_path}|" .env
sed -i "s|MICROBIN_PORT=.*|MICROBIN_PORT=${port}|" .env
# Start Microbin using Docker Compose
docker compose --env-file .env up --detach

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 KiB

View file

@ -1,78 +0,0 @@
module MBP
module BasicSyntaxHighlighter
# Plugin Properties
def self.get_id()
"BasicSyntaxHighlighter"
end
def self.get_name()
"Basic Syntax Highlighter Plugin"
end
def self.get_version()
"1.0.0"
end
def self.get_author()
"Daniel Szabo"
end
def self.get_webpage()
"https://github.com/szabodanika/microbin"
end
def self.get_description()
"This plugin will simply color keywords and special characters in four different colors based on some very basic RegEx - it is meant to univesally make code pastas more readable but is not a robust syntax highlighter solution."
end
# Plugin Event Hooks
def self.init()
# Ignore event
"OK"
end
def self.on_pasta_created(content)
# We do not modify stored content
return content
end
def self.on_pasta_read(content)
tokens = {
"orchid" => [/([0-9])/, /([t|T][r|R][u|U][e|E]|[f|F][a|A][l|L][s|S][e|E])/],
"palevioletred" => ['(', ')', '{', '}', '[', ']'],
"royalblue" => [/(\s(for|while|do|select|async|await|mut|break|continue|in|as|switch|let|fn|async|if|else|elseif|new|switch|match|case|default|public|protected|private|return|class|interface|static|final|const|var|int|integer|boolean|float|double|module|def|end|void))(?![a-z])/],
"mediumorchid" => [/(:|\.|;|=|>|<|\?|!|#|%|@|\^|&|\*|\|)/],
"mediumseagreen" => [/(\".*\")/, /(\'.*\')/]
};
tokens.each { | color, tokens |
for token in tokens do
if(token.class == String)
content.gsub!(token, "$$#{color}$$" + token + "$$/#{color}$$")
elsif
content.gsub!(token, "$$#{color}$$" + '\1' + "$$/#{color}$$")
end
end
};
tokens.each { | color, tokens |
content.gsub!("$$#{color}$$", "<span style='color:#{color}'>");
content.gsub!("$$/#{color}$$", "</span>");
};
return content
end
end
end

View file

@ -1,78 +0,0 @@
require 'rutie'
module MB
class MBPlugin
# Plugin Properties
def self.get_id()
"[Enter Plugin ID]"
end
def self.get_name()
"[Eenter Plugin Name]"
end
def self.get_version()
"1.0.0"
end
def self.get_author()
"[Enter Author name]"
end
def self.get_webpage()
"[Enter Web URL]"
end
def self.get_description()
"[Enter Description]"
end
# Plugin Event Hooks
def self.init()
raise "Operation not supported";
end
def self.on_pasta_deleted(id, content, created, expiration, file)
raise "Operation not supported";
end
def self.on_pasta_expired(id, content, created, expiration, file)
raise "Operation not supported";
end
def self.on_pasta_created(id, content, created, expiration, file)
raise "Operation not supported";
end
def self.on_pasta_read(id, content, created, expiration, file)
raise "Operation not supported";
end
# Rust Function Calls
def self.init()
raise "Operation not supported";
end
def self.P=on_pasta_deleted(id, content, created, expiration, file)
raise "Operation not supported";
end
def self.on_pasta_expired(id, content, created, expiration, file)
raise "Operation not supported";
end
def self.on_pasta_created(id, content, created, expiration, file)
raise "Operation not supported";
end
def self.on_pasta_read(id, content, created, expiration, file)
raise "Operation not supported";
end
end
end

View file

@ -1,42 +0,0 @@
module MBP
class HelloWorld < MBPlugin
def self.get_id()
"HelloWorld"
end
def self.get_name()
"Hello World Plugin"
end
def self.get_version()
"1.0.0"
end
def self.get_description()
"This is just a demo plugin. It does not do anything."
end
def self.get_author()
"Daniel Szabo"
end
def self.get_webpage()
"https://github.com/szabodanika/microbin"
end
def self.init()
# Ignore event
"OK"
end
def self.on_pasta_created(content)
return content
end
def self.on_pasta_read(content)
return content
end
end
end

9
render.yaml Normal file
View file

@ -0,0 +1,9 @@
services:
- type: web
name: microbin
plan: free
numInstances: 1
env: rust
repo: https://github.com/szabodanika/microbin.git
buildCommand: cargo build --release
startCommand: ./target/release/microbin --editable --highlightsyntax

View file

@ -1,48 +0,0 @@
const ANIMAL_NAMES: &[&str] = &[
"ant", "eel", "mole", "sloth", "ape", "emu", "monkey", "snail", "bat", "falcon", "mouse",
"snake", "bear", "fish", "otter", "spider", "bee", "fly", "parrot", "squid", "bird", "fox",
"panda", "swan", "bison", "frog", "pig", "tiger", "camel", "gecko", "pigeon", "toad", "cat",
"goat", "pony", "turkey", "cobra", "goose", "pug", "turtle", "crow", "hawk", "rabbit", "viper",
"deer", "horse", "rat", "wasp", "dog", "jaguar", "raven", "whale", "dove", "koala", "seal",
"wolf", "duck", "lion", "shark", "worm", "eagle", "lizard", "sheep", "zebra",
];
pub fn to_animal_names(mut number: u64) -> String {
let mut result: Vec<&str> = Vec::new();
if number == 0 {
return ANIMAL_NAMES[0].parse().unwrap();
}
// max 4 animals so 6 * 6 = 64 bits
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;
}
}
result.join("-")
}
pub fn to_u64(animal_names: &str) -> u64 {
let mut result: u64 = 0;
let animals: Vec<&str> = animal_names.split("-").collect();
let mut pow = animals.len();
for i in 0..animals.len() {
pow -= 1;
result += (ANIMAL_NAMES.iter().position(|&r| r == animals[i]).unwrap()
* ANIMAL_NAMES.len().pow(pow as u32)) as u64;
}
result
}

234
src/args.rs Normal file
View file

@ -0,0 +1,234 @@
use clap::Parser;
use lazy_static::lazy_static;
use serde::Serialize;
use std::convert::Infallible;
use std::fmt;
use std::net::IpAddr;
use std::str::FromStr;
lazy_static! {
pub static ref ARGS: Args = Args::parse();
}
#[derive(Parser, Debug, Clone, Serialize)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, env = "MICROBIN_BASIC_AUTH_USERNAME")]
pub auth_basic_username: Option<String>,
#[clap(long, env = "MICROBIN_BASIC_AUTH_PASSWORD")]
pub auth_basic_password: Option<String>,
#[clap(long, env = "MICROBIN_ADMIN_USERNAME", default_value = "admin")]
pub auth_admin_username: String,
#[clap(long, env = "MICROBIN_ADMIN_PASSWORD", default_value = "m1cr0b1n")]
pub auth_admin_password: String,
#[clap(long, env = "MICROBIN_EDITABLE")]
pub editable: bool,
#[clap(long, env = "MICROBIN_FOOTER_TEXT")]
pub footer_text: Option<String>,
#[clap(long, env = "MICROBIN_HIDE_FOOTER")]
pub hide_footer: bool,
#[clap(long, env = "MICROBIN_HIDE_HEADER")]
pub hide_header: bool,
#[clap(long, env = "MICROBIN_HIDE_LOGO")]
pub hide_logo: bool,
#[clap(long, env = "MICROBIN_NO_LISTING")]
pub no_listing: bool,
#[clap(long, env = "MICROBIN_HIGHLIGHTSYNTAX")]
pub highlightsyntax: bool,
#[clap(short, long, env = "MICROBIN_PORT", default_value_t = 8080)]
pub port: u16,
#[clap(short, long, env="MICROBIN_BIND", default_value_t = IpAddr::from([0, 0, 0, 0]))]
pub bind: IpAddr,
#[clap(long, env = "MICROBIN_PRIVATE")]
pub private: bool,
#[clap(long, env = "MICROBIN_PURE_HTML")]
pub pure_html: bool,
#[clap(long, env = "MICROBIN_JSON_DB")]
pub json_db: bool,
#[clap(long, env = "MICROBIN_PUBLIC_PATH")]
pub public_path: Option<PublicUrl>,
#[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,
#[clap(long, env = "MICROBIN_SHOW_READ_STATS")]
pub show_read_stats: bool,
#[clap(long, env = "MICROBIN_TITLE")]
pub title: Option<String>,
#[clap(short, long, env = "MICROBIN_THREADS", default_value_t = 1)]
pub threads: u8,
#[clap(short, long, env = "MICROBIN_GC_DAYS", default_value_t = 90)]
pub gc_days: u16,
#[clap(long, env = "MICROBIN_ENABLE_BURN_AFTER")]
pub enable_burn_after: bool,
#[clap(short, long, env = "MICROBIN_DEFAULT_BURN_AFTER", default_value_t = 0)]
pub default_burn_after: u16,
#[clap(long, env = "MICROBIN_WIDE")]
pub wide: bool,
#[clap(long, env = "MICROBIN_QR")]
pub qr: bool,
#[clap(long, env = "MICROBIN_ETERNAL_PASTA")]
pub eternal_pasta: bool,
#[clap(long, env = "MICROBIN_ENABLE_READONLY")]
pub enable_readonly: bool,
#[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,
#[clap(long, env = "MICROBIN_CUSTOM_CSS")]
pub custom_css: Option<String>,
#[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,
#[clap(long, env = "MICROBIN_ENCRYPTION_SERVER_SIDE")]
pub encryption_server_side: bool,
#[clap(
long,
env = "MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB",
default_value_t = 256
)]
pub max_file_size_encrypted_mb: usize,
#[clap(
long,
env = "MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB",
default_value_t = 2048
)]
pub max_file_size_unencrypted_mb: usize,
}
impl Args {
pub fn public_path_as_str(&self) -> String {
if self.public_path.is_some() {
self.public_path.as_ref().unwrap().to_string()
} else {
String::from("")
}
}
pub fn short_path_as_str(&self) -> String {
if self.short_path.is_some() {
self.short_path.as_ref().unwrap().to_string()
} else if self.public_path.is_some() {
self.public_path.as_ref().unwrap().to_string()
} else {
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, Serialize)]
pub struct PublicUrl(pub String);
impl fmt::Display for PublicUrl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for PublicUrl {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let uri = s.strip_suffix('/').unwrap_or(s).to_owned();
Ok(PublicUrl(uri))
}
}

View file

@ -1,53 +0,0 @@
use std::fs::File;
use std::io;
use std::io::{BufReader, BufWriter};
use crate::Pasta;
static DATABASE_PATH: &'static str = "pasta_data/database.json";
pub 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)
}
}
}
}
}
pub fn load_from_file() -> io::Result<Vec<Pasta>> {
let file = File::open(DATABASE_PATH);
match file {
Ok(_) => {
let reader = BufReader::new(file.unwrap());
let data: Vec<Pasta> = serde_json::from_reader(reader).unwrap();
Ok(data)
}
Err(_) => {
log::info!("Database file {} not found!", DATABASE_PATH);
save_to_file(&Vec::<Pasta>::new());
log::info!("Database file {} created.", DATABASE_PATH);
load_from_file()
}
}
}

106
src/endpoints/admin.rs Normal file
View file

@ -0,0 +1,106 @@
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};
use askama::Template;
use futures::TryStreamExt;
#[derive(Template)]
#[template(path = "admin.html")]
struct AdminTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
status: &'a String,
version_string: &'a String,
message: &'a String,
update: &'a Option<Version>,
}
#[get("/admin")]
pub async fn get_admin() -> Result<HttpResponse, Error> {
return Ok(HttpResponse::Found()
.append_header(("Location", "/auth_admin"))
.finish());
}
#[post("/admin")]
pub async fn post_admin(
data: web::Data<AppState>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
let mut username = String::from("");
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
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() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
}
}
if username != ARGS.auth_admin_username || password != ARGS.auth_admin_password {
return Ok(HttpResponse::Found()
.append_header(("Location", "/auth_admin/incorrect"))
.finish());
}
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
// sort pastas in reverse-chronological order of creation time
pastas.sort_by(|a, b| b.created.cmp(&a.created));
// todo status report more sophisticated
let mut status = "OK";
let mut message = "";
if ARGS.public_path.is_none() {
status = "WARNING";
message = "Warning: No public URL set with --public-path parameter. QR code and URL Copying functions have been disabled"
}
if ARGS.auth_admin_username == "admin" && ARGS.auth_admin_password == "m1cr0b1n" {
status = "WARNING";
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: &format!("{}", CURRENT_VERSION.long_title),
message: &String::from(message),
update: &update,
}
.render()
.unwrap(),
))
}

View file

@ -0,0 +1,36 @@
use crate::args::{Args, ARGS};
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "auth_admin.html")]
struct AuthAdmin<'a> {
args: &'a Args,
status: String,
}
#[get("/auth_admin")]
pub async fn auth_admin() -> HttpResponse {
return HttpResponse::Ok().content_type("text/html").body(
AuthAdmin {
args: &ARGS,
status: String::from(""),
}
.render()
.unwrap(),
);
}
#[get("/auth_admin/{status}")]
pub async fn auth_admin_with_status(param: web::Path<String>) -> HttpResponse {
let status = param.into_inner();
return HttpResponse::Ok().content_type("text/html").body(
AuthAdmin {
args: &ARGS,
status,
}
.render()
.unwrap(),
);
}

View file

@ -0,0 +1,394 @@
use crate::args::{Args, ARGS};
use crate::endpoints::errors::ErrorTemplate;
use crate::util::animalnumbers::to_u64;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::remove_expired;
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "auth_upload.html")]
struct AuthPasta<'a> {
args: &'a Args,
id: String,
status: String,
encrypted_key: String,
encrypt_client: bool,
path: String,
}
#[get("/auth/{id}")]
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();
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 (_i, 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("upload"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth/{id}/{status}")]
pub async fn auth_upload_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("upload"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_raw/{id}")]
pub async fn auth_raw_pasta(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 (_i, 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("raw"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_raw/{id}/{status}")]
pub async fn auth_raw_pasta_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("raw"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_edit_private/{id}")]
pub async fn auth_edit_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("edit_private"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_edit_private/{id}/{status}")]
pub async fn auth_edit_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("edit_private"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_file/{id}")]
pub async fn auth_file(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("secure_file"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/auth_file/{id}/{status}")]
pub async fn auth_file_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("secure_file"),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.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())
}

340
src/endpoints/create.rs Normal file
View file

@ -0,0 +1,340 @@
use crate::pasta::PastaFile;
use crate::util::animalnumbers::to_animal_names;
use crate::util::db::insert;
use crate::util::hashids::to_hashids;
use crate::util::misc::{encrypt, encrypt_file, is_valid_url};
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
use actix_web::error::ErrorBadRequest;
use actix_web::{get, web, Error, HttpResponse, Responder};
use askama::Template;
use bytesize::ByteSize;
use futures::TryStreamExt;
use log::warn;
use rand::Rng;
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[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,
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 {
match expiration {
"1min" => timenow + 60,
"10min" => timenow + 60 * 10,
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"3days" => timenow + 60 * 60 * 24 * 3,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => {
if ARGS.eternal_pasta {
0
} else {
timenow + 60 * 60 * 24 * 7
}
}
_ => {
log::error!("{}", "Unexpected expiration time!");
timenow + 60 * 60 * 24 * 7
}
}
}
/// 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> {
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from(""),
file: None,
extension: String::from(""),
private: false,
readonly: false,
editable: ARGS.editable,
encrypt_server: false,
encrypted_key: Some(String::from("")),
encrypt_client: false,
created: timenow,
read_count: 0,
burn_after_reads: 0,
last_read: timenow,
pasta_type: String::from(""),
expiration: expiration_to_timestamp(&ARGS.default_expiry, timenow),
};
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? {
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();
}
continue;
}
"privacy" => {
while let Some(chunk) = field.try_next().await? {
let privacy = std::str::from_utf8(&chunk).unwrap();
new_pasta.private = match privacy {
"public" => false,
_ => true,
};
new_pasta.readonly = match privacy {
"readonly" => true,
_ => false,
};
new_pasta.encrypt_client = match privacy {
"secret" => true,
_ => false,
};
new_pasta.encrypt_server = match privacy {
"private" => true,
"secret" => true,
_ => false,
};
}
}
"plain_key" => {
while let Some(chunk) = field.try_next().await? {
plain_key = std::str::from_utf8(&chunk).unwrap().to_string();
}
continue;
}
"encrypted_random_key" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.encrypted_key =
Some(std::str::from_utf8(&chunk).unwrap().to_string());
}
continue;
}
"expiration" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.expiration =
expiration_to_timestamp(std::str::from_utf8(&chunk).unwrap(), timenow);
}
continue;
}
"burn_after" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.burn_after_reads = match std::str::from_utf8(&chunk).unwrap() {
// give an extra read because the user will be
// redirected to the pasta page automatically
"1" => 2,
"10" => 10,
"100" => 100,
"1000" => 1000,
"10000" => 10000,
"0" => 0,
_ => {
log::error!("{}", "Unexpected burn after value!");
0
}
};
}
continue;
}
"content" => {
let mut content = String::from("");
while let Some(chunk) = field.try_next().await? {
content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
if !content.is_empty() {
new_pasta.content = content;
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
String::from("text")
};
}
continue;
}
"syntax_highlight" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
}
continue;
}
"file" => {
if ARGS.no_file_upload {
continue;
}
let path = field.content_disposition().and_then(|cd| cd.get_filename());
let path = match path {
Some("") => continue,
Some(p) => p,
None => continue,
};
let mut file = match PastaFile::from_unsanitized(path) {
Ok(f) => f,
Err(e) => {
warn!("Unsafe file name: {e:?}");
continue;
}
};
std::fs::create_dir_all(format!(
"{}/attachments/{}",
ARGS.data_dir,
&new_pasta.id_as_animals()
))
.unwrap();
let filepath = format!(
"{}/attachments/{}/{}",
ARGS.data_dir,
&new_pasta.id_as_animals(),
&file.name()
);
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
let mut size = 0;
while let Some(chunk) = field.try_next().await? {
size += chunk.len();
if (new_pasta.encrypt_server
&& size > ARGS.max_file_size_encrypted_mb * 1024 * 1024)
|| size > ARGS.max_file_size_unencrypted_mb * 1024 * 1024
{
return Err(ErrorBadRequest("File exceeded size limit."));
}
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
file.size = ByteSize::b(size as u64);
new_pasta.file = Some(file);
new_pasta.pasta_type = String::from("text");
}
field => {
log::error!("Unexpected multipart field: {}", field);
}
}
}
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 {
new_pasta.encrypted_key = Some(encrypt(id.to_string().as_str(), &plain_key));
}
if new_pasta.encrypt_server && !new_pasta.readonly && new_pasta.content != *"" {
if new_pasta.encrypt_client {
new_pasta.content = encrypt(&new_pasta.content, &random_key);
} else {
new_pasta.content = encrypt(&new_pasta.content, &plain_key);
}
}
if new_pasta.file.is_some() && new_pasta.encrypt_server && !new_pasta.readonly {
let filepath = format!(
"{}/attachments/{}/{}",
ARGS.data_dir,
&new_pasta.id_as_animals(),
&new_pasta.file.as_ref().unwrap().name()
);
if new_pasta.encrypt_client {
encrypt_file(&random_key, &filepath).expect("Failed to encrypt file with random key")
} else {
encrypt_file(&plain_key, &filepath).expect("Failed to encrypt file with plain key")
}
}
let encrypt_server = new_pasta.encrypt_server;
pastas.push(new_pasta);
for (_, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
insert(Some(&pastas), Some(pasta));
}
}
let slug = if ARGS.hash_ids {
to_hashids(id)
} else {
to_animal_names(id)
};
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())
}
}

384
src/endpoints/edit.rs Normal file
View file

@ -0,0 +1,384 @@
use crate::args::Args;
use crate::endpoints::errors::ErrorTemplate;
use crate::util::animalnumbers::to_u64;
use crate::util::db::update;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::{decrypt, encrypt, remove_expired};
use crate::{AppState, Pasta, ARGS};
use actix_multipart::Multipart;
use actix_web::{get, post, web, Error, HttpResponse};
use askama::Template;
use futures::TryStreamExt;
#[derive(Template)]
#[template(path = "edit.html", escape = "none")]
struct EditTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
path: &'a String,
status: &'a String,
}
#[get("/edit/{id}")]
pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if !pasta.editable {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish();
}
if pasta.encrypt_server {
return HttpResponse::Found()
.append_header((
"Location",
format!("/auth_edit_private/{}", pasta.id_as_animals()),
))
.finish();
}
return HttpResponse::Ok().content_type("text/html").body(
EditTemplate {
pasta,
args: &ARGS,
path: &String::from("edit"),
status: &String::from(""),
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/edit/{id}/{status}")]
pub async fn get_edit_with_status(
data: web::Data<AppState>,
param: web::Path<(String, String)>,
) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
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)
};
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == intern_id {
if !pasta.editable {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish();
}
if pasta.encrypt_server {
return HttpResponse::Found()
.append_header((
"Location",
format!("/auth_edit_private/{}", pasta.id_as_animals()),
))
.finish();
}
return HttpResponse::Ok().content_type("text/html").body(
EditTemplate {
pasta,
args: &ARGS,
path: &String::from("edit"),
status: &status,
}
.render()
.unwrap(),
);
}
}
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[post("/edit_private/{id}")]
pub async fn post_edit_private(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
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());
}
}
}
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found && !pastas[index].encrypt_client {
let original_content = pastas[index].content.to_owned();
// decrypt content temporarily
if password != *"" {
let res = decrypt(&original_content, &password);
if res.is_ok() {
pastas[index]
.content
.replace_range(.., res.unwrap().as_str());
// save pasta in database
update(Some(&pastas), Some(&pastas[index]));
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!(
"/auth_edit_private/{}/incorrect",
pastas[index].id_as_animals()
),
))
.finish());
}
}
// serve pasta in template
let response = HttpResponse::Ok().content_type("text/html").body(
EditTemplate {
pasta: &pastas[index],
args: &ARGS,
path: &String::from("submit_edit_private"),
status: &String::from(""),
}
.render()
.unwrap(),
);
if pastas[index].content != original_content {
pastas[index].content = original_content;
}
return Ok(response);
}
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}
#[post("/submit_edit_private/{id}")]
pub async fn post_submit_edit_private(
data: web::Data<AppState>,
id: web::Path<String>,
mut payload: Multipart,
) -> Result<HttpResponse, Error> {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
let mut password = String::from("");
let mut new_content = String::from("");
while let Some(mut field) = payload.try_next().await? {
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() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password = std::str::from_utf8(&chunk).unwrap().to_string();
}
}
}
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
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() {
pastas[index]
.content
.replace_range(.., &encrypt(&new_content, &password));
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/edit/{}/incorrect", pastas[index].id_as_animals()),
))
.finish());
}
} else if pastas[index].private {
let res = decrypt(&pastas[index].content, &password);
if res.is_ok() {
pastas[index]
.content
.replace_range(.., &encrypt(&new_content, &password));
// save pasta in database
update(Some(&pastas), Some(&pastas[index]));
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!(
"/auth_edit_private/{}/incorrect",
pastas[index].id_as_animals()
),
))
.finish());
}
}
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth/{}/success", pastas[index].id_as_animals()),
))
.finish());
}
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}
#[post("/edit/{id}")]
pub async fn post_edit(
data: web::Data<AppState>,
id: web::Path<String>,
mut 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 mut new_content = String::from("");
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
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() == Some("password") {
while let Some(chunk) = field.try_next().await? {
password = std::str::from_utf8(&chunk).unwrap().to_string();
}
}
}
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
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);
if res.is_ok() {
pastas[i].content.replace_range(.., &new_content);
// save pasta in database
update(Some(&pastas), Some(&pastas[i]));
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/edit/{}/incorrect", pasta.id_as_animals()),
))
.finish());
}
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/edit/{}/incorrect", pasta.id_as_animals()),
))
.finish());
}
} else {
pastas[i].content.replace_range(.., &new_content);
// save pasta in database
update(Some(&pastas), Some(&pastas[i]));
}
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!(
"{}/upload/{}",
ARGS.public_path_as_str(),
pastas[i].id_as_animals()
),
))
.finish());
} else {
break;
}
}
}
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}

16
src/endpoints/errors.rs Normal file
View file

@ -0,0 +1,16 @@
use actix_web::{Error, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
#[derive(Template)]
#[template(path = "error.html")]
pub struct ErrorTemplate<'a> {
pub args: &'a Args,
}
pub async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}

142
src/endpoints/file.rs Normal file
View file

@ -0,0 +1,142 @@
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};
#[post("/secure_file/{id}")]
pub async fn post_secure_file(
data: web::Data<AppState>,
id: web::Path<String>,
payload: Multipart,
) -> Result<HttpResponse, Error> {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
let password = auth::password_from_multipart(payload).await?;
if found {
if let Some(ref pasta_file) = pastas[index].file {
let file = File::open(format!(
"{}/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
let content_type = mime_guess::from_path(&pasta_file.name)
.first_or_octet_stream()
.to_string();
// Create a response with the decrypted data
let response = HttpResponse::Ok()
.content_type(content_type)
.append_header((
"Content-Disposition",
format!("attachment; filename=\"{}\"", pasta_file.name()),
))
// TODO: make streaming <21-10-24, dvdsk>
.body(decrypted_data);
return Ok(response);
}
}
Ok(HttpResponse::NotFound().finish())
}
#[get("/file/{id}")]
pub async fn get_file(
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();
let id_intern = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id_intern {
index = i;
found = true;
break;
}
}
if found {
if let Some(ref pasta_file) = pastas[index].file {
if pastas[index].encrypt_server {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth_file/{}", pastas[index].id_as_animals()),
))
.finish());
}
// Construct the path to the file
let file_path = format!(
"{}/attachments/{}/{}",
ARGS.data_dir,
pastas[index].id_as_animals(),
pasta_file.name()
);
let file_path = PathBuf::from(file_path);
// 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));
}
}
Ok(HttpResponse::NotFound().finish())
}

16
src/endpoints/guide.rs Normal file
View file

@ -0,0 +1,16 @@
use crate::args::{Args, ARGS};
use actix_web::{get, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "guide.html")]
struct Guide<'a> {
args: &'a Args,
}
#[get("/guide")]
pub async fn guide() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html")
.body(Guide { args: &ARGS }.render().unwrap())
}

39
src/endpoints/list.rs Normal file
View file

@ -0,0 +1,39 @@
use actix_web::{get, web, HttpResponse};
use askama::Template;
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::util::misc::remove_expired;
use crate::AppState;
#[derive(Template)]
#[template(path = "list.html")]
struct ListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/list")]
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
// sort pastas in reverse-chronological order of creation time
pastas.sort_by(|a, b| b.created.cmp(&a.created));
HttpResponse::Ok().content_type("text/html").body(
ListTemplate {
pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
}

405
src/endpoints/pasta.rs Normal file
View file

@ -0,0 +1,405 @@
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;
use crate::AppState;
use actix_multipart::Multipart;
use actix_web::{get, post, web, Error, HttpResponse};
use askama::Template;
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "upload.html", escape = "none")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
fn pastaresponse(
data: web::Data<AppState>,
id: web::Path<String>,
password: String,
) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
if pastas[index].encrypt_server && password == *"" {
return HttpResponse::Found()
.append_header((
"Location",
format!("/auth/{}", pastas[index].id_as_animals()),
))
.finish();
}
// increment read count
pastas[index].read_count += 1;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
let original_content = pastas[index].content.to_owned();
// decrypt content temporarily
if password != *"" && !original_content.is_empty() {
let res = decrypt(&original_content, &password);
if let Ok(..) = res {
pastas[index]
.content
.replace_range(.., res.unwrap().as_str());
} else {
return HttpResponse::Found()
.append_header((
"Location",
format!("/auth/{}/incorrect", pastas[index].id_as_animals()),
))
.finish();
}
}
// serve pasta in template
let response = HttpResponse::Ok().content_type("text/html").body(
PastaTemplate {
pasta: &pastas[index],
args: &ARGS,
}
.render()
.unwrap(),
);
if pastas[index].content != original_content {
pastas[index].content = original_content;
}
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
return response;
}
// otherwise send pasta not found error
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[post("/upload/{id}")]
pub async fn postpasta(
data: web::Data<AppState>,
id: web::Path<String>,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let password = auth::password_from_multipart(payload).await?;
Ok(pastaresponse(data, id, password))
}
#[post("/p/{id}")]
pub async fn postshortpasta(
data: web::Data<AppState>,
id: web::Path<String>,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let password = auth::password_from_multipart(payload).await?;
Ok(pastaresponse(data, id, password))
}
#[get("/upload/{id}")]
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
pastaresponse(data, id, String::from(""))
}
#[get("/p/{id}")]
pub async fn getshortpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
pastaresponse(data, id, String::from(""))
}
fn urlresponse(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
// increment read count
pastas[index].read_count += 1;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
// send redirect if it's a url pasta
if pastas[index].pasta_type == "url" {
let response = HttpResponse::Found()
.append_header(("Location", String::from(&pastas[index].content)))
.finish();
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
return response;
// send error if we're trying to open a non-url pasta as a redirect
} else {
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
}
}
// otherwise send pasta not found error
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/url/{id}")]
pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
urlresponse(data, id)
}
#[get("/u/{id}")]
pub async fn shortredirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
urlresponse(data, id)
}
#[get("/raw/{id}")]
pub async fn getrawpasta(
data: web::Data<AppState>,
id: web::Path<String>,
) -> Result<HttpResponse, Error> {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
if pastas[index].encrypt_server {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth_raw/{}", pastas[index].id_as_animals()),
))
.finish());
}
// increment read count
pastas[index].read_count += 1;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
// send raw content of pasta
let response = Ok(HttpResponse::NotFound()
.content_type("text/plain; charset=utf-8")
.body(pastas[index].content.to_owned()));
return response;
}
// otherwise send pasta not found error as raw text
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(String::from("Upload not found! :-(")))
}
#[post("/raw/{id}")]
pub async fn postrawpasta(
data: web::Data<AppState>,
id: web::Path<String>,
payload: Multipart,
) -> Result<HttpResponse, Error> {
let password = auth::password_from_multipart(payload).await?;
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
index = i;
found = true;
break;
}
}
if found {
if pastas[index].encrypt_server && password == *"" {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth/{}", pastas[index].id_as_animals()),
))
.finish());
}
// increment read count
pastas[index].read_count += 1;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
let original_content = pastas[index].content.to_owned();
// decrypt content temporarily
if password != *"" {
let res = decrypt(&original_content, &password);
if res.is_ok() {
pastas[index]
.content
.replace_range(.., res.unwrap().as_str());
} else {
return Ok(HttpResponse::Found()
.append_header((
"Location",
format!("/auth/{}/incorrect", pastas[index].id_as_animals()),
))
.finish());
}
}
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// update last read time
pastas[index].last_read = timenow;
// save the updated read count
update(Some(&pastas), Some(&pastas[index]));
// send raw content of pasta
let response = Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(pastas[index].content.to_owned()));
if pastas[index].content != original_content {
pastas[index].content = original_content;
}
return response;
}
// otherwise send pasta not found error as raw text
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(String::from("Upload not found! :-(")))
}
fn decrypt(text_str: &str, key_str: &str) -> Result<String, magic_crypt::MagicCryptError> {
let mc = new_magic_crypt!(key_str, 256);
mc.decrypt_base64_to_string(text_str)
}

72
src/endpoints/qr.rs Normal file
View file

@ -0,0 +1,72 @@
use crate::args::{Args, ARGS};
use crate::endpoints::errors::ErrorTemplate;
use crate::pasta::Pasta;
use crate::util::animalnumbers::to_u64;
use crate::util::hashids::to_u64 as hashid_to_u64;
use crate::util::misc::{self, remove_expired};
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "qr.html", escape = "none")]
struct QRTemplate<'a> {
qr: &'a String,
pasta: &'a Pasta,
args: &'a Args,
}
#[get("/qr/{id}")]
pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
let u64_id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
remove_expired(&mut pastas);
// find the index of the pasta in the collection based on u64 id
let mut index: usize = 0;
let mut found: bool = false;
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == u64_id {
index = i;
found = true;
break;
}
}
if found {
// 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!("{}/upload/{}", &ARGS.public_path_as_str(), &id).as_str(),
),
};
// serve qr code in template
return HttpResponse::Ok().content_type("text/html").body(
QRTemplate {
qr: &svg,
pasta: &pastas[index],
args: &ARGS,
}
.render()
.unwrap(),
);
}
// otherwise
// send pasta not found error
HttpResponse::Ok()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}

175
src/endpoints/remove.rs Normal file
View file

@ -0,0 +1,175 @@
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::{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 {
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&id.into_inner()).unwrap_or(0)
};
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!(
"{}/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 HttpResponse::Found()
.append_header(("Location", format!("{}/list", ARGS.public_path_as_str())))
.finish();
}
}
remove_expired(&mut pastas);
HttpResponse::Ok()
.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

@ -0,0 +1,21 @@
use actix_web::{web, HttpResponse, Responder};
use mime_guess::from_path;
use rust_embed::RustEmbed;
#[derive(RustEmbed)]
#[folder = "templates/assets/"]
struct Asset;
fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => HttpResponse::Ok()
.content_type(from_path(path).first_or_octet_stream().as_ref())
.body(content.data.into_owned()),
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
#[actix_web::get("/static/{_:.*}")]
async fn static_resources(path: web::Path<String>) -> impl Responder {
handle_embedded_file(path.as_str())
}

View file

@ -1,353 +1,63 @@
extern crate core;
use crate::args::ARGS;
use crate::endpoints::{
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;
use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;
use std::fs;
use std::io::Write;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use actix_files;
use actix_multipart::Multipart;
use actix_web::dev::ServiceRequest;
use actix_web::middleware::Condition;
use actix_web::{error, get, middleware, web, App, Error, HttpResponse, HttpServer, Responder};
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::middleware::HttpAuthentication;
use askama::Template;
use chrono::Local;
use clap::Parser;
use futures::TryStreamExt as _;
use lazy_static::lazy_static;
use linkify::{LinkFinder, LinkKind};
use log::LevelFilter;
use rand::Rng;
use std::fs;
pub mod args;
pub mod pasta;
use crate::animalnumbers::{to_animal_names, to_u64};
use crate::dbio::save_to_file;
use crate::pasta::Pasta;
mod animalnumbers;
mod dbio;
mod pasta;
mod plugins;
lazy_static! {
static ref ARGS: Args = Args::parse();
pub mod util {
pub mod animalnumbers;
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;
}
struct AppState {
pastas: Mutex<Vec<Pasta>>,
pub mod endpoints {
pub mod admin;
pub mod auth_admin;
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 qr;
pub mod remove;
pub mod static_resources;
}
#[derive(Parser, Debug, Clone)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long, default_value_t = 8080)]
port: u32,
#[clap(short, long, default_value_t = 1)]
threads: u8,
#[clap(short, long)]
wide: bool,
#[clap(short, long, default_value_t = 3)]
animals: u8,
#[clap(long)]
hide_header: bool,
#[clap(long)]
hide_footer: bool,
#[clap(long)]
pure_html: bool,
#[clap(long)]
no_listing: bool,
#[clap(long)]
auth_username: Option<String>,
#[clap(long)]
auth_password: Option<String>,
}
async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_username.as_ref().unwrap() {
return match ARGS.auth_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."))
}
}
#[derive(Template)]
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a Args,
}
#[derive(Template)]
#[template(path = "error.html")]
struct ErrorTemplate<'a> {
args: &'a Args,
}
#[derive(Template)]
#[template(path = "pasta.html", escape = "none")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/")]
async fn index() -> impl Responder {
HttpResponse::Found()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
}
async fn not_found() -> Result<HttpResponse, Error> {
Ok(HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap()))
}
async fn create(data: web::Data<AppState>, mut payload: Multipart) -> Result<HttpResponse, Error> {
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
} as i64;
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
file: String::from("no-file"),
created: timenow,
pasta_type: String::from(""),
expiration: 0,
};
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"expiration" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.expiration = match std::str::from_utf8(&chunk).unwrap() {
"1min" => timenow + 60,
"10min" => timenow + 60 * 10,
"1hour" => timenow + 60 * 60,
"24hour" => timenow + 60 * 60 * 24,
"1week" => timenow + 60 * 60 * 24 * 7,
"never" => 0,
_ => panic!("Unexpected expiration time!"),
};
}
continue;
}
"content" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.content =
plugins::on_pasta_created(std::str::from_utf8(&chunk).unwrap());
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
String::from("url")
} else {
String::from("text")
};
}
continue;
}
"file" => {
let content_disposition = field.content_disposition();
let filename = match content_disposition.get_filename() {
Some("") => continue,
Some(filename) => filename.replace(' ', "_").to_string(),
None => continue,
};
std::fs::create_dir_all(format!("./pasta_data/{}", &new_pasta.id_as_animals()))
.unwrap();
let filepath = format!("./pasta_data/{}/{}", &new_pasta.id_as_animals(), &filename);
new_pasta.file = filename;
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
while let Some(chunk) = field.try_next().await? {
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
new_pasta.pasta_type = String::from("text");
}
_ => {}
}
}
let id = new_pasta.id;
pastas.push(new_pasta);
save_to_file(&pastas);
Ok(HttpResponse::Found()
.append_header(("Location", format!("/pasta/{}", to_animal_names(id))))
.finish())
}
#[get("/pasta/{id}")]
async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
let pasta_copy = Pasta {
id: pasta.id,
content: plugins::on_pasta_read(&pasta.content),
file: pasta.file.to_string(),
created: pasta.created,
pasta_type: pasta.pasta_type.to_string(),
expiration: pasta.expiration,
};
return HttpResponse::Found().content_type("text/html").body(
PastaTemplate {
pasta: &pasta_copy,
args: &ARGS,
}
.render()
.unwrap(),
);
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/url/{id}")]
async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
if pasta.pasta_type == "url" {
return HttpResponse::Found()
.append_header(("Location", String::from(&pasta.content)))
.finish();
} else {
return HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap());
}
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/raw/{id}")]
async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> String {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for pasta in pastas.iter() {
if pasta.id == id {
return pasta.content.to_owned();
}
}
String::from("Pasta not found! :-(")
}
#[get("/remove/{id}")]
async fn remove(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
let mut pastas = data.pastas.lock().unwrap();
let id = to_u64(&*id.into_inner());
remove_expired(&mut pastas);
for (i, pasta) in pastas.iter().enumerate() {
if pasta.id == id {
pastas.remove(i);
return HttpResponse::Found()
.append_header(("Location", "/pastalist"))
.finish();
}
}
HttpResponse::Found()
.content_type("text/html")
.body(ErrorTemplate { args: &ARGS }.render().unwrap())
}
#[get("/pastalist")]
async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", "/"))
.finish();
}
let mut pastas = data.pastas.lock().unwrap();
remove_expired(&mut pastas);
HttpResponse::Found().content_type("text/html").body(
PastaListTemplate {
pastas: &pastas,
args: &ARGS,
}
.render()
.unwrap(),
)
pub struct AppState {
pub pastas: Mutex<Vec<Pasta>>,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let args: Args = Args::parse();
Builder::new()
.format(|buf, record| {
writeln!(
@ -362,73 +72,86 @@ async fn main() -> std::io::Result<()> {
.init();
log::info!(
"MicroBin starting on http://127.0.0.1:{}",
args.port.to_string()
"MicroBin starting on http://{}:{}",
ARGS.bind.to_string(),
ARGS.port.to_string()
);
match std::fs::create_dir_all("./pasta_data") {
match fs::create_dir_all(format!("{}/public", ARGS.data_dir)) {
Ok(dir) => dir,
Err(error) => {
log::error!("Couldn't create data directory ./pasta_data: {:?}", error);
panic!("Couldn't create data directory ./pasta_data: {:?}", error);
log::error!(
"Couldn't create data directory {}/attachments/: {:?}",
ARGS.data_dir,
error
);
panic!(
"Couldn't create data directory {}/attachments/: {:?}",
ARGS.data_dir, error
);
}
};
let data = web::Data::new(AppState {
pastas: Mutex::new(dbio::load_from_file().unwrap()),
pastas: Mutex::new(read_all()),
});
if !ARGS.disable_telemetry {
start_telemetry_thread();
}
HttpServer::new(move || {
App::new()
.app_data(data.clone())
.wrap(middleware::NormalizePath::trim())
.service(index)
.service(getpasta)
.service(redirecturl)
.service(getrawpasta)
.service(actix_files::Files::new("/static", "./static"))
.service(actix_files::Files::new("/file", "./pasta_data"))
.service(web::resource("/upload").route(web::post().to(create)))
.default_service(web::route().to(not_found))
.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_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)
.service(pasta_endpoint::postshortpasta)
.service(pasta_endpoint::getrawpasta)
.service(pasta_endpoint::postrawpasta)
.service(pasta_endpoint::redirecturl)
.service(pasta_endpoint::shortredirecturl)
.service(edit::get_edit)
.service(edit::get_edit_with_status)
.service(edit::post_edit)
.service(edit::post_edit_private)
.service(edit::post_submit_edit_private)
.service(admin::get_admin)
.service(admin::post_admin)
.service(static_resources::static_resources)
.service(qr::getqr)
.service(file::get_file)
.service(file::post_secure_file)
.service(web::resource("/upload").route(web::post().to(create::create)))
.default_service(web::route().to(errors::not_found))
.wrap(middleware::Logger::default())
.service(remove)
.service(list)
.service(remove::remove)
.service(remove::post_remove)
.service(list::list)
.service(create::index_with_status)
.wrap(Condition::new(
args.auth_username.is_some(),
HttpAuthentication::basic(auth_validator),
ARGS.auth_basic_username.is_some()
&& ARGS.auth_basic_username.as_ref().unwrap().trim() != "",
HttpAuthentication::basic(util::auth::auth_validator),
))
})
.bind(format!("0.0.0.0:{}", args.port.to_string()))?
.workers(args.threads as usize)
.bind((ARGS.bind, ARGS.port))?
.workers(ARGS.threads as usize)
.run()
.await
}
fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
} as i64;
pastas.retain(|p| {
// expiration is `never` or not reached
if p.expiration == 0 || p.expiration > timenow {
// keep
true
} else {
// remove the file itself
fs::remove_file(format!("./pasta_data/{}/{}", p.id_as_animals(), p.file));
// and remove the containing directory
fs::remove_dir(format!("./pasta_data/{}/", p.id_as_animals()));
// remove
false
}
});
}
fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}

View file

@ -1,51 +1,257 @@
use std::fmt;
use chrono::{DateTime, Datelike, NaiveDateTime, Timelike, Utc};
use bytesize::ByteSize;
use chrono::{Datelike, Local, TimeZone, Timelike};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::to_animal_names;
use crate::args::ARGS;
use crate::util::animalnumbers::to_animal_names;
use crate::util::hashids::to_hashids;
use crate::util::syntaxhighlighter::html_highlight;
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
pub struct PastaFile {
pub name: String,
pub size: ByteSize,
}
impl PastaFile {
pub fn from_unsanitized(path: &str) -> Result<Self, &'static str> {
let path = Path::new(path);
let name = path.file_name().ok_or("Path did not contain a file name")?;
let name = name.to_string_lossy().replace(' ', "_");
Ok(Self {
name,
size: ByteSize::b(0),
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn is_image(&self) -> bool {
let lowercase_name = self.name.to_lowercase();
let extensions = [
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".ico", ".svg", ".tiff", ".tif",
".jfif", ".pjpeg", ".pjp", ".avif", ".jxl", ".heif",
];
extensions.iter().any(|&ext| lowercase_name.ends_with(ext))
}
pub fn is_video(&self) -> bool {
let lowercase_name = self.name.to_lowercase();
let extensions = [
".mp4", ".mov", ".wmv", ".webm", ".avi", ".flv", ".mkv", ".mts",
];
extensions.iter().any(|&ext| lowercase_name.ends_with(ext))
}
pub fn embeddable(&self) -> bool {
self.is_image() || self.is_video()
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Pasta {
pub id: u64,
pub content: String,
pub file: String,
pub file: Option<PastaFile>,
pub extension: String,
pub private: bool,
pub readonly: bool,
pub editable: bool,
pub encrypt_server: bool,
pub encrypt_client: bool,
pub encrypted_key: Option<String>,
pub created: i64,
pub expiration: i64,
pub last_read: i64,
pub read_count: u64,
pub burn_after_reads: u64,
pub pasta_type: String,
}
impl Pasta {
pub fn id_as_animals(&self) -> String {
to_animal_names(self.id)
if ARGS.hash_ids {
to_hashids(self.id)
} else {
to_animal_names(self.id)
}
}
pub fn has_file(&self) -> bool {
self.file.is_some()
}
pub fn total_size_as_string(&self) -> String {
let total_size_bytes = if self.has_file() {
self.file.as_ref().unwrap().size.as_u64() as usize + self.content.as_bytes().len()
} else {
self.content.as_bytes().len()
};
if total_size_bytes < 1024 {
format!("{} B", total_size_bytes)
} else if total_size_bytes < 1024 * 1024 {
format!("{} KB", total_size_bytes / 1024)
} else if total_size_bytes < 1024 * 1024 * 1024 {
format!("{} MB", total_size_bytes / (1024 * 1024))
} else {
format!("{} GB", total_size_bytes / (1024 * 1024 * 1024))
}
}
pub fn file_embeddable(&self) -> bool {
return self.has_file()
&& self.file.as_ref().unwrap().embeddable()
&& !(self.encrypt_server || self.encrypt_client);
}
pub fn created_as_string(&self) -> String {
let date = DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.created, 0), Utc);
format!(
"{:02}-{:02} {}:{}",
date.month(),
date.day(),
date.hour(),
date.minute(),
)
Local.timestamp_opt(self.created, 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 created date");
String::from("Unknow")
})
}
pub fn expiration_as_string(&self) -> String {
if self.expiration == 0 {
String::from("Never")
} else {
let date =
DateTime::<Utc>::from_utc(NaiveDateTime::from_timestamp(self.expiration, 0), Utc);
format!(
"{:02}-{:02} {}:{}",
date.month(),
date.day(),
date.hour(),
date.minute(),
)
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")
})
}
}
pub fn last_read_time_ago_as_string(&self) -> String {
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// get seconds since last read and convert it to days
let days = ((timenow - self.last_read) / 86400) as u16;
if days > 1 {
return format!("{} days ago", days);
};
// it's less than 1 day, let's do hours then
let hours = ((timenow - self.last_read) / 3600) as u16;
if hours > 1 {
return format!("{} hours ago", hours);
};
// it's less than 1 hour, let's do minutes then
let minutes = ((timenow - self.last_read) / 60) as u16;
if minutes > 1 {
return format!("{} minutes ago", minutes);
};
// it's less than 1 minute, let's do seconds then
let seconds = (timenow - self.last_read) as u16;
if seconds > 1 {
return format!("{} seconds ago", seconds);
};
// it's less than 1 second?????
String::from("just now")
}
pub fn short_last_read_time_ago_as_string(&self) -> String {
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// get seconds since last read and convert it to days
let days = ((timenow - self.last_read) / 86400) as u16;
if days > 1 {
return format!("{} d ago", days);
};
// it's less than 1 day, let's do hours then
let hours = ((timenow - self.last_read) / 3600) as u16;
if hours > 1 {
return format!("{} h ago", hours);
};
// it's less than 1 hour, let's do minutes then
let minutes = ((timenow - self.last_read) / 60) as u16;
if minutes > 1 {
return format!("{} m ago", minutes);
};
// it's less than 1 minute, let's do seconds then
let seconds = (timenow - self.last_read) as u16;
if seconds > 1 {
return format!("{} s ago", seconds);
};
// it's less than 1 second?????
String::from("just now")
}
pub fn last_read_days_ago(&self) -> u16 {
// get current unix time in seconds
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
// get seconds since last read and convert it to days
((timenow - self.last_read) / 86400) as u16
}
pub fn content_syntax_highlighted(&self) -> String {
html_highlight(&self.content, &self.extension)
}
pub fn content_not_highlighted(&self) -> String {
html_highlight(&self.content, "txt")
}
pub fn content_escaped(&self) -> String {
html_escape::encode_text(
&self
.content
.replace('\\', "\\\\")
.replace('`', "\\`")
.replace('$', "\\$"),
)
.to_string()
}
}
impl fmt::Display for Pasta {

View file

@ -1,134 +0,0 @@
extern crate rutie;
use lazy_static::lazy_static;
use log::{error, log};
use rutie::{AnyException, AnyObject, Object, RString, VM};
use std::fs::File;
use std::io::{BufReader, Read};
use std::{fs, io};
const CACHE_PLUGINS: bool = false;
lazy_static! {
static ref PLUGIN_IDENTIFIERS: Vec<String> = init();
}
fn init() -> Vec<String> {
VM::init();
let plugin_paths = load_plugin_paths();
let plugin_codes = read_plugins(plugin_paths.clone());
feed_plugins(plugin_codes);
let identifiers = get_plugin_identifiers(plugin_paths);
init_plugins(&identifiers);
identifiers
}
pub fn pasta_filter(s: &str) -> bool {
true
}
pub fn on_pasta_read(s: &str) -> String {
let mut processed_content: String = String::from(s);
for PLUGIN_IDENTIFIER in PLUGIN_IDENTIFIERS.iter() {
processed_content = eval_for_string(PLUGIN_IDENTIFIER, "on_pasta_read", s);
}
processed_content
}
pub fn on_pasta_created(s: &str) -> String {
let mut processed_content: String = String::from(s);
for PLUGIN_IDENTIFIER in PLUGIN_IDENTIFIERS.iter() {
processed_content = eval_for_string(PLUGIN_IDENTIFIER, "on_pasta_created", s);
}
processed_content
}
pub fn init_plugins(plugin_identifiers: &Vec<String>) {
for PLUGIN_IDENTIFIER in plugin_identifiers.iter() {
eval_for_string(PLUGIN_IDENTIFIER, "init", "");
let init_result = eval_for_string(&PLUGIN_IDENTIFIER, "init", "");
let id = eval_for_string(&PLUGIN_IDENTIFIER, "get_id", "");
let name = eval_for_string(&id, "get_name", "");
let version = eval_for_string(&id, "get_version", "");
log::info!("Initialised plugin {id} - {name} ({version})");
}
}
fn eval_for_string(plugin_id: &str, function: &str, parameter: &str) -> String {
match VM::eval(&*format!("MBP::{}::{}({})", plugin_id, function, parameter)) {
Ok(result) => match result.try_convert_to::<RString>() {
Ok(ruby_string) => ruby_string.to_string(),
Err(err) => err.to_string(),
},
Err(err) => {
log::error!(
"Failed to run function '{}' on plugin {}: {}",
function,
plugin_id,
err
);
err.to_string()
}
}
}
fn load_plugin_paths() -> Vec<String> {
let paths = fs::read_dir("./plugins").expect("Failed to access ./plugins library.");
let mut plugin_paths: Vec<String> = Vec::new();
for path in paths {
plugin_paths.push(path.unwrap().path().to_str().unwrap().parse().unwrap());
}
plugin_paths
}
fn read_plugins(plugin_paths: Vec<String>) -> Vec<String> {
let mut plugin_codes: Vec<String> = Vec::new();
for plugin_path in plugin_paths {
let plugin_code = match fs::read_to_string(&plugin_path) {
Ok(result) => result,
Err(err) => {
log::error!("Failed to read plugin file {}: {}", plugin_path, err);
continue;
}
};
plugin_codes.push(plugin_code);
}
plugin_codes
}
fn feed_plugins(plugin_codes: Vec<String>) {
for plugin_code in plugin_codes {
match VM::eval(plugin_code.as_str()) {
Ok(result) => {}
Err(error) => {
log::error!("Failed to initialise plugin: {}", error);
continue;
}
}
}
}
fn get_plugin_identifiers(plugin_paths: Vec<String>) -> Vec<String> {
let mut plugin_ids: Vec<String> = Vec::new();
for plugin_path in plugin_paths {
plugin_ids.push(plugin_path.replace("./plugins/", "").replace(".rb", ""))
}
plugin_ids
}

62
src/util/animalnumbers.rs Normal file
View file

@ -0,0 +1,62 @@
const ANIMAL_NAMES: &[&str] = &[
"ant", "eel", "mole", "sloth", "ape", "emu", "monkey", "snail", "bat", "falcon", "mouse",
"snake", "bear", "fish", "otter", "spider", "bee", "fly", "parrot", "squid", "bird", "fox",
"panda", "swan", "bison", "frog", "pig", "tiger", "camel", "gecko", "pigeon", "toad", "cat",
"goat", "pony", "turkey", "cobra", "goose", "pug", "turtle", "crow", "hawk", "rabbit", "viper",
"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(number: u64) -> String {
let mut result: Vec<&str> = Vec::new();
if number == 0 {
return ANIMAL_NAMES[0].parse().unwrap();
}
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;
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(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));
}

38
src/util/auth.rs Normal file
View file

@ -0,0 +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,
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)
}

70
src/util/db.rs Normal file
View file

@ -0,0 +1,70 @@
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()
} else {
super::db_sqlite::read_all()
}
}
#[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);
}
}

52
src/util/db_json.rs Normal file
View file

@ -0,0 +1,52 @@
use std::fs::File;
use std::io;
use std::io::{BufReader, BufWriter};
use crate::Pasta;
static DATABASE_PATH: &str = "pasta_data/database.json";
pub fn read_all() -> Vec<Pasta> {
load_from_file().expect("Failed to load pastas from JSON")
}
pub fn update_all(pastas: &Vec<Pasta>) {
save_to_file(pastas);
}
fn save_to_file(pasta_data: &Vec<Pasta>) {
// 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>> {
let file = File::open(DATABASE_PATH);
match file {
Ok(_) => {
let reader = BufReader::new(file.unwrap());
let data: Vec<Pasta> = match serde_json::from_reader(reader) {
Ok(t) => t,
_ => Vec::new(),
};
Ok(data)
}
Err(_) => {
log::info!("Database file {} not found!", DATABASE_PATH);
save_to_file(&Vec::<Pasta>::new());
log::info!("Database file {} created.", DATABASE_PATH);
load_from_file()
}
}
}

297
src/util/db_sqlite.rs Normal file
View file

@ -0,0 +1,297 @@
use bytesize::ByteSize;
use rusqlite::{params, Connection};
use crate::{args::ARGS, pasta::PastaFile, Pasta};
pub fn read_all() -> Vec<Pasta> {
select_all_from_db()
}
pub fn update_all(pastas: &[Pasta]) {
rewrite_all_to_db(pastas);
}
pub fn rewrite_all_to_db(pasta_data: &[Pasta]) {
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"
DROP TABLE IF EXISTS pasta;
);",
params![],
)
.expect("Failed to drop SQLite table for Pasta!");
conn.execute(
"
CREATE TABLE IF NOT EXISTS pasta (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
file_name TEXT,
file_size INTEGER,
extension TEXT NOT NULL,
read_only INTEGER NOT NULL,
private INTEGER NOT NULL,
editable INTEGER NOT NULL,
encrypt_server INTEGER NOT NULL,
encrypt_client INTEGER NOT NULL,
encrypted_key TEXT,
created INTEGER NOT NULL,
expiration INTEGER NOT NULL,
last_read INTEGER NOT NULL,
read_count INTEGER NOT NULL,
burn_after_reads INTEGER NOT NULL,
pasta_type TEXT NOT NULL
);",
params![],
)
.expect("Failed to create SQLite table for Pasta!");
for pasta in pasta_data.iter() {
conn.execute(
"INSERT INTO pasta (
id,
content,
file_name,
file_size,
extension,
private,
read_only,
editable,
encrypt_server,
encrypt_client,
encrypted_key,
created,
expiration,
last_read,
read_count,
burn_after_reads,
pasta_type
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
params![
pasta.id,
pasta.content,
pasta.file.as_ref().map_or("", |f| f.name.as_str()),
pasta.file.as_ref().map_or(0, |f| f.size.as_u64()),
pasta.extension,
pasta.private as i32,
pasta.readonly as i32,
pasta.editable as i32,
pasta.encrypt_server as i32,
pasta.encrypt_client as i32,
pasta.encrypted_key.as_deref(),
pasta.created,
pasta.expiration,
pasta.last_read,
pasta.read_count,
pasta.burn_after_reads,
pasta.pasta_type,
],
)
.expect("Failed to insert pasta.");
}
}
pub fn select_all_from_db() -> Vec<Pasta> {
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"
CREATE TABLE IF NOT EXISTS pasta (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
file_name TEXT,
file_size INTEGER,
extension TEXT NOT NULL,
read_only INTEGER NOT NULL,
private INTEGER NOT NULL,
editable INTEGER NOT NULL,
encrypt_server INTEGER NOT NULL,
encrypt_client INTEGER NOT NULL,
encrypted_key TEXT,
created INTEGER NOT NULL,
expiration INTEGER NOT NULL,
last_read INTEGER NOT NULL,
read_count INTEGER NOT NULL,
burn_after_reads INTEGER NOT NULL,
pasta_type TEXT NOT NULL
);",
params![],
)
.expect("Failed to create SQLite table for Pasta!");
let mut stmt = conn
.prepare("SELECT * FROM pasta ORDER BY created ASC")
.expect("Failed to prepare SQL statement to load pastas");
let pasta_iter = stmt
.query_map([], |row| {
Ok(Pasta {
id: row.get(0)?,
content: row.get(1)?,
file: if let (Some(file_name), Some(file_size)) = (row.get(2)?, row.get(3)?) {
let file_size: u64 = file_size;
if file_name != "" && file_size != 0 {
Some(PastaFile {
name: file_name,
size: ByteSize::b(file_size),
})
} else {
None
}
} else {
None
},
extension: row.get(4)?,
readonly: row.get(5)?,
private: row.get(6)?,
editable: row.get(7)?,
encrypt_server: row.get(8)?,
encrypt_client: row.get(9)?,
encrypted_key: row.get(10)?,
created: row.get(11)?,
expiration: row.get(12)?,
last_read: row.get(13)?,
read_count: row.get(14)?,
burn_after_reads: row.get(15)?,
pasta_type: row.get(16)?,
})
})
.expect("Failed to select Pastas from SQLite database.");
pasta_iter
.map(|r| r.expect("Failed to get pasta"))
.collect::<Vec<Pasta>>()
}
pub fn insert(pasta: &Pasta) {
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"
CREATE TABLE IF NOT EXISTS pasta (
id INTEGER PRIMARY KEY,
content TEXT NOT NULL,
file_name TEXT,
file_size INTEGER,
extension TEXT NOT NULL,
read_only INTEGER NOT NULL,
private INTEGER NOT NULL,
editable INTEGER NOT NULL,
encrypt_server INTEGER NOT NULL,
encrypt_client INTEGER NOT NULL,
encrypted_key TEXT,
created INTEGER NOT NULL,
expiration INTEGER NOT NULL,
last_read INTEGER NOT NULL,
read_count INTEGER NOT NULL,
burn_after_reads INTEGER NOT NULL,
pasta_type TEXT NOT NULL
);",
params![],
)
.expect("Failed to create SQLite table for Pasta!");
conn.execute(
"INSERT INTO pasta (
id,
content,
file_name,
file_size,
extension,
read_only,
private,
editable,
encrypt_server,
encrypt_client,
encrypted_key,
created,
expiration,
last_read,
read_count,
burn_after_reads,
pasta_type
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17)",
params![
pasta.id,
pasta.content,
pasta.file.as_ref().map_or("", |f| f.name.as_str()),
pasta.file.as_ref().map_or(0, |f| f.size.as_u64()),
pasta.extension,
pasta.readonly as i32,
pasta.private as i32,
pasta.editable as i32,
pasta.encrypt_server as i32,
pasta.encrypt_client as i32,
pasta.encrypted_key.as_deref(),
pasta.created,
pasta.expiration,
pasta.last_read,
pasta.read_count,
pasta.burn_after_reads,
pasta.pasta_type,
],
)
.expect("Failed to insert pasta.");
}
pub fn update(pasta: &Pasta) {
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"UPDATE pasta SET
content = ?2,
file_name = ?3,
file_size = ?4,
extension = ?5,
read_only = ?6,
private = ?7,
editable = ?8,
encrypt_server = ?9,
encrypt_client = ?10,
encrypted_key = ?11,
created = ?12,
expiration = ?13,
last_read = ?14,
read_count = ?15,
burn_after_reads = ?16,
pasta_type = ?17
WHERE id = ?1;",
params![
pasta.id,
pasta.content,
pasta.file.as_ref().map_or("", |f| f.name.as_str()),
pasta.file.as_ref().map_or(0, |f| f.size.as_u64()),
pasta.extension,
pasta.readonly as i32,
pasta.private as i32,
pasta.editable as i32,
pasta.encrypt_server as i32,
pasta.encrypt_client as i32,
pasta.encrypted_key.as_deref(),
pasta.created,
pasta.expiration,
pasta.last_read,
pasta.read_count,
pasta.burn_after_reads,
pasta.pasta_type,
],
)
.expect("Failed to update pasta.");
}
pub fn delete_by_id(id: u64) {
let conn = Connection::open(format!("{}/database.sqlite", ARGS.data_dir))
.expect("Failed to open SQLite database!");
conn.execute(
"DELETE FROM pasta
WHERE id = ?1;",
params![id],
)
.expect("Failed to delete pasta.");
}

18
src/util/hashids.rs Normal file
View file

@ -0,0 +1,18 @@
use harsh::Harsh;
use lazy_static::lazy_static;
lazy_static! {
pub static ref HARSH: Harsh = Harsh::builder().length(6).build().unwrap();
}
pub fn to_hashids(number: u64) -> String {
HARSH.encode(&[number])
}
pub fn to_u64(hash_id: &str) -> Result<u64, &str> {
let ids = HARSH
.decode(hash_id)
.map_err(|_e| "Failed to decode hash ID")?;
let id = ids.first().ok_or("No ID found in hash ID")?;
Ok(*id)
}

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()
}

150
src/util/misc.rs Normal file
View file

@ -0,0 +1,150 @@
use crate::args::ARGS;
use linkify::{LinkFinder, LinkKind};
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
use qrcode_generator::QrCodeEcc;
use std::fs::{self, File};
use std::io::{BufReader, Read, Write};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::Pasta;
use super::db::delete;
pub fn remove_expired(pastas: &mut Vec<Pasta>) {
// get current time - this will be needed to check which pastas have expired
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(_) => {
log::error!("SystemTime before UNIX EPOCH!");
0
}
} as i64;
pastas.retain(|p| {
// keep if:
// expiration is `never` or not reached
// AND
// read count is less than burn limit, or no limit set
// AND
// has been read in the last N days where N is the arg --gc-days OR N is 0 (no GC)
if (p.expiration == 0 || p.expiration > timenow)
&& (p.read_count < p.burn_after_reads || p.burn_after_reads == 0)
&& (p.last_read_days_ago() < ARGS.gc_days || ARGS.gc_days == 0)
{
// keep
true
} else {
// remove from database
delete(None, Some(p.id));
// remove the file itself
if let Some(file) = &p.file {
if fs::remove_file(format!(
"{}/attachments/{}/{}",
ARGS.data_dir,
p.id_as_animals(),
file.name()
))
.is_err()
{
log::error!("Failed to delete file {}!", file.name())
}
// and remove the containing directory
if fs::remove_dir(format!(
"{}/attachments/{}/",
ARGS.data_dir,
p.id_as_animals()
))
.is_err()
{
log::error!("Failed to delete directory {}!", file.name())
}
}
false
}
});
}
pub fn string_to_qr_svg(str: &str) -> String {
qrcode_generator::to_svg_to_string(str, QrCodeEcc::Low, 256, None::<&str>).unwrap()
}
pub fn is_valid_url(url: &str) -> bool {
let finder = LinkFinder::new();
let spans: Vec<_> = finder.spans(url).collect();
spans[0].as_str() == url && Some(&LinkKind::Url) == spans[0].kind()
}
pub fn encrypt(text_str: &str, key_str: &str) -> String {
if text_str.is_empty() {
return String::from("");
}
let mc = new_magic_crypt!(key_str, 256);
mc.encrypt_str_to_base64(text_str)
}
pub fn decrypt(text_str: &str, key_str: &str) -> Result<String, magic_crypt::MagicCryptError> {
if text_str.is_empty() {
return Ok(String::from(""));
}
let mc = new_magic_crypt!(key_str, 256);
mc.decrypt_base64_to_string(text_str)
}
pub fn encrypt_file(
passphrase: &str,
input_file_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
// Read the input file into memory
let file = File::open(input_file_path).expect("Tried to encrypt non-existent file");
let mut reader = BufReader::new(file);
let mut input_data = Vec::new();
reader.read_to_end(&mut input_data)?;
// Create a MagicCrypt instance with the given passphrase
let mc = new_magic_crypt!(passphrase, 256);
// Encrypt the input data
let ciphertext = mc.encrypt_bytes_to_bytes(&input_data[..]);
// Write the encrypted data to a new file with the .enc extension
let mut f = File::create(
Path::new(input_file_path)
.with_file_name("data")
.with_extension("enc"),
)?;
f.write_all(ciphertext.as_slice())?;
// Delete the original input file
// input_file.seek(SeekFrom::Start(0))?;
fs::remove_file(input_file_path)?;
Ok(())
}
pub fn decrypt_file(
passphrase: &str,
input_file: &File,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
// Read the input file into memory
let mut reader = BufReader::new(input_file);
let mut ciphertext = Vec::new();
reader.read_to_end(&mut ciphertext)?;
// Create a MagicCrypt instance with the given passphrase
let mc = new_magic_crypt!(passphrase, 256);
// Encrypt the input data
let res = mc.decrypt_bytes_to_bytes(&ciphertext[..]);
if res.is_err() {
return Err("Failed to decrypt file".into());
}
Ok(res.unwrap())
}

View file

@ -0,0 +1,37 @@
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style, ThemeSet};
use syntect::html::append_highlighted_html_for_styled_line;
use syntect::html::IncludeBackground::No;
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
pub fn html_highlight(text: &str, extension: &str) -> String {
let ps = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
let syntax = ps
.find_syntax_by_extension(extension)
.or_else(|| Option::from(ps.find_syntax_plain_text()))
.unwrap();
let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]);
let mut highlighted_content: String = String::from("");
for line in LinesWithEndings::from(text) {
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
append_highlighted_html_for_styled_line(&ranges[..], No, &mut highlighted_content)
.expect("Failed to append highlighted line!");
}
let mut highlighted_content2: String = String::from("");
for line in highlighted_content.lines() {
highlighted_content2 += &*format!("<code-line>{}</code-line>\n", line);
}
// Rewrite colours to ones that are compatible with water.css and both light/dark modes
highlighted_content2 = highlighted_content2.replace("style=\"color:#323232;\"", "");
highlighted_content2 =
highlighted_content2.replace("style=\"color:#183691;\"", "style=\"color:blue;\"");
highlighted_content2
}

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)
}

File diff suppressed because one or more lines are too long

448
templates/admin.html Normal file
View file

@ -0,0 +1,448 @@
{% include "header.html" %}
<h2>Welcome to MicroBin</h2>
<div style="height: 200px;">
<div style="float: left">
<h4>Links</h4>
<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>
<a href="https://github.com/szabodanika/microbin/issues" style="margin-right: 1rem">Feedback</a>
<br>
<a href="https://microbin.eu/donate">Donate and Sponsor</a>
</div>
<div style="float: right">
<h4>Info</h4>
<table style="width: 400px">
<tr>
<td><b>Version</b></td>
<td>{{version_string}} </td>
</tr>
<tr>
<td><b>Status</b></td>
<td>{{status}} </td>
</tr>
<tr>
<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>
{%- endif %}
<h3>Uploads</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%;">
{% else %}
<table style="width: 100%; font-size: smaller;">
{% endif %}
<thead>
<th style="width: 15%;">
Key
</th>
<th style="width: 15%;">
Valid
</th>
<th style="width: 8%;">
Size
</th>
<th>
Encryption
</th>
<th style="width: 5%;">
Priv.
</th>
<th style="width: 5%;">
Edit.
</th>
<th style="width: 8%;">
Content
</th>
<th>
Hits
</th>
<th style="width: 8%;">
<!-- Actions -->
</th>
</thead>
<tbody>
{% for pasta in pastas %}
{% if pasta.pasta_type == "text" %}
<tr>
<td>
<a
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
{{pasta.expiration_as_string()}}
</td>
<td>
{{pasta.total_size_as_string()}}
</td>
<td>
{% if pasta.encrypt_client %}
CLIENT
{%- endif %}
{% if pasta.encrypt_client && pasta.encrypt_server%}
+
{%- endif %}
{% if pasta.encrypt_server %}
SERVER
{%- endif %}
</td>
<td>
{% if pasta.private %}
✔️
{%- endif %}
</td>
<td>
{% if pasta.editable %}
✔️
{%- endif %}
</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 args.show_read_stats %} {% if pasta.read_count == 1 %}
<span style="font-size: small">{{pasta.read_count}} hits <br> last
{{pasta.short_last_read_time_ago_as_string()}}</span>
{%- else %}
<span style="font-size: small">{{pasta.read_count}} hits <br> last
{{pasta.short_last_read_time_ago_as_string()}}</span>
{%- endif %} {%- endif %}
</td>
<td>
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path_as_str()
}}/edit/{{pasta.id_as_animals()}}">Edit</a>
<br>
{%- endif %}
<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%; font-size: smaller;">
{% endif %}
<thead>
<th style="width: 15%;">
Key
</th>
<th style="width: 15%;">
Valid
</th>
<th>
Encryption
</th>
<th style="width: 5%;">
Priv.
</th>
<th style="width: 5%;">
Edit.
</th>
<th style="width: 8%;">
Content
</th>
<th>
Hits
</th>
<th style="width: 8%;">
<!-- Actions -->
</th>
</thead>
<tbody>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" %}
<tr>
<td>
<a
href="{{ args.public_path_as_str()}}/upload/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
{{pasta.expiration_as_string()}}
</td>
<td>
{% if pasta.encrypt_client %}
CLIENT
{%- endif %}
{% if pasta.encrypt_client && pasta.encrypt_server%}
+
{%- endif %}
{% if pasta.encrypt_server %}
SERVER
{%- endif %}
</td>
<td>
{% if pasta.private %}
✔️
{%- endif %}
</td>
<td>
{% if pasta.editable %}
✔️
{%- endif %}
</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 args.show_read_stats %} {% if pasta.read_count == 1 %}
<span style="font-size: small">{{pasta.read_count}} hits <br> last
{{pasta.short_last_read_time_ago_as_string()}}</span>
{%- else %}
<span style="font-size: small">{{pasta.read_count}} hits <br> last
{{pasta.short_last_read_time_ago_as_string()}}</span>
{%- endif %} {%- endif %}
</td>
<td>
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path_as_str()
}}/edit/{{pasta.id_as_animals()}}">Edit</a>
<br>
{%- endif %}
<a href="{{ args.public_path_as_str() }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<h3>Environmental Variables</h3>
<table>
<thead>
<tr>
<th style="width: 35%">Argument</th>
<th style="width: 15%">Value</th>
<th style="width: 35%">Argument</th>
<th style="width: 15%">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>auth_basic_username</td>
{% if args.auth_basic_username.as_ref().is_some() %}
<td>set</td>
{% else %}
<td>unset</td>
{% endif %}
<td>auth_basic_password</td>
{% if args.auth_basic_password.as_ref().is_some() %}
<td>set</td>
{% else %}
<td>unset</td>
{% endif %}
</tr>
<tr>
<td>editable</td>
<td>{{ args.editable }}</td>
<td>footer_text</td>
{% if args.footer_text.as_ref().is_some() %}
<td>{{ args.footer_text.as_ref().unwrap() }}</td>
{% else %}
<td>unset</td>
{% endif %}
</tr>
<tr>
<td>hide_footer</td>
<td>{{ args.hide_footer }}</td>
<td>hide_header</td>
<td>{{ args.hide_header }}</td>
</tr>
<tr>
<td>hide_logo</td>
<td>{{ args.hide_logo }}</td>
<td>no_listing</td>
<td>{{ args.no_listing }}</td>
</tr>
<tr>
<td>highlightsyntax</td>
<td>{{ args.highlightsyntax }}</td>
<td>port</td>
<td>{{ args.port }}</td>
</tr>
<tr>
<td>bind</td>
<td>{{ args.bind }}</td>
<td>private</td>
<td>{{ args.private }}</td>
</tr>
<tr>
<td>pure_html</td>
<td>{{ args.pure_html }}</td>
<td>json_db</td>
<td>{{ args.json_db }}</td>
</tr>
<tr>
<td>public_path</td>
{% if args.public_path.as_ref().is_some() %}
<td>{{ args.public_path_as_str() }}</td>
{% else %}
<td>unset</td>
{% endif %}
<td>short_path</td>
{% if args.short_path.as_ref().is_some() %}
<td>{{ args.short_path_as_str() }}</td>
{% else %}
<td>unset</td>
{% endif %}
</tr>
<tr>
<td>readonly</td>
<td>{{ args.readonly }}</td>
<td>show_read_stats</td>
<td>{{ args.show_read_stats }}</td>
</tr>
<tr>
<td>title</td>
{% if args.title.as_ref().is_some() %}
<td>{{ args.title.as_ref().unwrap() }}</td>
{% else %}
<td>unset</td>
{% endif %}
<td>threads</td>
<td>{{ args.threads }}</td>
</tr>
<tr>
<td>gc_days</td>
<td>{{ args.gc_days }}</td>
<td>enable_burn_after</td>
<td>{{ args.enable_burn_after }}</td>
</tr>
<tr>
<td>default_burn_after</td>
<td>{{ args.default_burn_after }}</td>
<td>wide</td>
<td>{{ args.wide }}</td>
</tr>
<tr>
<td>qr</td>
<td>{{ args.qr }}</td>
<td>eternal_pasta</td>
<td>{{ args.eternal_pasta }}</td>
</tr>
<tr>
<td>enable_readonly</td>
<td>{{ args.enable_readonly }}</td>
<td>default_expiry</td>
<td>{{ args.default_expiry }}</td>
</tr>
<tr>
<td>no_file_upload</td>
<td>{{ args.no_file_upload }}</td>
<td>custom_css</td>
{% if args.custom_css.as_ref().is_some() %}
<td>{{ args.custom_css.as_ref().unwrap() }}</td>
{% else %}
<td>unset</td>
{% endif %}
</tr>
<tr>
<td>hash_ids</td>
<td>{{ args.hash_ids }}</td>
<td>encryption_client_side</td>
<td>{{ args.encryption_client_side }}</td>
</tr>
<tr>
<td>encryption_server_side</td>
<td>{{ args.encryption_server_side }}</td>
<td>max_file_size_encrypted_mb</td>
<td>{{ args.max_file_size_encrypted_mb }} MB</td>
</tr>
<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>
{% include "footer.html" %}
<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>
</style>

803
templates/assets/aes.js Normal file
View file

@ -0,0 +1,803 @@
/*! MIT License. Copyright 2015-2018 Richard Moore <me@ricmoo.com>. See LICENSE.txt. */
(function(root) {
"use strict";
function checkInt(value) {
return (parseInt(value) === value);
}
function checkInts(arrayish) {
if (!checkInt(arrayish.length)) { return false; }
for (var i = 0; i < arrayish.length; i++) {
if (!checkInt(arrayish[i]) || arrayish[i] < 0 || arrayish[i] > 255) {
return false;
}
}
return true;
}
function coerceArray(arg, copy) {
// ArrayBuffer view
if (arg.buffer && arg.name === 'Uint8Array') {
if (copy) {
if (arg.slice) {
arg = arg.slice();
} else {
arg = Array.prototype.slice.call(arg);
}
}
return arg;
}
// It's an array; check it is a valid representation of a byte
if (Array.isArray(arg)) {
if (!checkInts(arg)) {
throw new Error('Array contains invalid value: ' + arg);
}
return new Uint8Array(arg);
}
// Something else, but behaves like an array (maybe a Buffer? Arguments?)
if (checkInt(arg.length) && checkInts(arg)) {
return new Uint8Array(arg);
}
throw new Error('unsupported array-like object');
}
function createArray(length) {
return new Uint8Array(length);
}
function copyArray(sourceArray, targetArray, targetStart, sourceStart, sourceEnd) {
if (sourceStart != null || sourceEnd != null) {
if (sourceArray.slice) {
sourceArray = sourceArray.slice(sourceStart, sourceEnd);
} else {
sourceArray = Array.prototype.slice.call(sourceArray, sourceStart, sourceEnd);
}
}
targetArray.set(sourceArray, targetStart);
}
var convertUtf8 = (function() {
function toBytes(text) {
var result = [], i = 0;
text = encodeURI(text);
while (i < text.length) {
var c = text.charCodeAt(i++);
// if it is a % sign, encode the following 2 bytes as a hex value
if (c === 37) {
result.push(parseInt(text.substr(i, 2), 16))
i += 2;
// otherwise, just the actual byte
} else {
result.push(c)
}
}
return coerceArray(result);
}
function fromBytes(bytes) {
var result = [], i = 0;
while (i < bytes.length) {
var c = bytes[i];
if (c < 128) {
result.push(String.fromCharCode(c));
i++;
} else if (c > 191 && c < 224) {
result.push(String.fromCharCode(((c & 0x1f) << 6) | (bytes[i + 1] & 0x3f)));
i += 2;
} else {
result.push(String.fromCharCode(((c & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f)));
i += 3;
}
}
return result.join('');
}
return {
toBytes: toBytes,
fromBytes: fromBytes,
}
})();
var convertHex = (function() {
function toBytes(text) {
var result = [];
for (var i = 0; i < text.length; i += 2) {
result.push(parseInt(text.substr(i, 2), 16));
}
return result;
}
// http://ixti.net/development/javascript/2011/11/11/base64-encodedecode-of-utf8-in-browser-with-js.html
var Hex = '0123456789abcdef';
function fromBytes(bytes) {
var result = [];
for (var i = 0; i < bytes.length; i++) {
var v = bytes[i];
result.push(Hex[(v & 0xf0) >> 4] + Hex[v & 0x0f]);
}
return result.join('');
}
return {
toBytes: toBytes,
fromBytes: fromBytes,
}
})();
// Number of rounds by keysize
var numberOfRounds = {16: 10, 24: 12, 32: 14}
// Round constant words
var rcon = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91];
// S-box and Inverse S-box (S is for Substitution)
var S = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];
var Si =[0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb, 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb, 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e, 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25, 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92, 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84, 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06, 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b, 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73, 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e, 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b, 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4, 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f, 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef, 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61, 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d];
// Transformations for encryption
var T1 = [0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x02010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x0804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0x0a05050f, 0x2f9a9ab5, 0x0e070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0x00000000, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x04020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x058f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0x0b888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0x0c06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x018d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0x0d8b8b86, 0x0f8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x06030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x078e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x038c8c8f, 0x59a1a1f8, 0x09898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a];
var T2 = [0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0x0dfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x03020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0x0bfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x02f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x08f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0x0c080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0x0f0a0505, 0xb52f9a9a, 0x090e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x00000000, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x06040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x04f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0x0efdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0x0a0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x07f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x05060303, 0x01f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616];
var T3 = [0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x01030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x040c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x050f0a05, 0x9ab52f9a, 0x07090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x091b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0x00000000, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x02060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0x0c14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0x0b1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0x0a1e140a, 0x49db9249, 0x060a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x08181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x03050603, 0xf601f7f6, 0x0e121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0x0d171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0x0f111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16];
var T4 = [0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x01010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x04040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x05050f0a, 0x9a9ab52f, 0x0707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x09091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0x00000000, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x02020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0x0c0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0x0b0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0x0a0a1e14, 0x4949db92, 0x06060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x08081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x03030506, 0xf6f601f7, 0x0e0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0x0d0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0x0f0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c];
// Transformations for decryption
var T5 = [0x51f4a750, 0x7e416553, 0x1a17a4c3, 0x3a275e96, 0x3bab6bcb, 0x1f9d45f1, 0xacfa58ab, 0x4be30393, 0x2030fa55, 0xad766df6, 0x88cc7691, 0xf5024c25, 0x4fe5d7fc, 0xc52acbd7, 0x26354480, 0xb562a38f, 0xdeb15a49, 0x25ba1b67, 0x45ea0e98, 0x5dfec0e1, 0xc32f7502, 0x814cf012, 0x8d4697a3, 0x6bd3f9c6, 0x038f5fe7, 0x15929c95, 0xbf6d7aeb, 0x955259da, 0xd4be832d, 0x587421d3, 0x49e06929, 0x8ec9c844, 0x75c2896a, 0xf48e7978, 0x99583e6b, 0x27b971dd, 0xbee14fb6, 0xf088ad17, 0xc920ac66, 0x7dce3ab4, 0x63df4a18, 0xe51a3182, 0x97513360, 0x62537f45, 0xb16477e0, 0xbb6bae84, 0xfe81a01c, 0xf9082b94, 0x70486858, 0x8f45fd19, 0x94de6c87, 0x527bf8b7, 0xab73d323, 0x724b02e2, 0xe31f8f57, 0x6655ab2a, 0xb2eb2807, 0x2fb5c203, 0x86c57b9a, 0xd33708a5, 0x302887f2, 0x23bfa5b2, 0x02036aba, 0xed16825c, 0x8acf1c2b, 0xa779b492, 0xf307f2f0, 0x4e69e2a1, 0x65daf4cd, 0x0605bed5, 0xd134621f, 0xc4a6fe8a, 0x342e539d, 0xa2f355a0, 0x058ae132, 0xa4f6eb75, 0x0b83ec39, 0x4060efaa, 0x5e719f06, 0xbd6e1051, 0x3e218af9, 0x96dd063d, 0xdd3e05ae, 0x4de6bd46, 0x91548db5, 0x71c45d05, 0x0406d46f, 0x605015ff, 0x1998fb24, 0xd6bde997, 0x894043cc, 0x67d99e77, 0xb0e842bd, 0x07898b88, 0xe7195b38, 0x79c8eedb, 0xa17c0a47, 0x7c420fe9, 0xf8841ec9, 0x00000000, 0x09808683, 0x322bed48, 0x1e1170ac, 0x6c5a724e, 0xfd0efffb, 0x0f853856, 0x3daed51e, 0x362d3927, 0x0a0fd964, 0x685ca621, 0x9b5b54d1, 0x24362e3a, 0x0c0a67b1, 0x9357e70f, 0xb4ee96d2, 0x1b9b919e, 0x80c0c54f, 0x61dc20a2, 0x5a774b69, 0x1c121a16, 0xe293ba0a, 0xc0a02ae5, 0x3c22e043, 0x121b171d, 0x0e090d0b, 0xf28bc7ad, 0x2db6a8b9, 0x141ea9c8, 0x57f11985, 0xaf75074c, 0xee99ddbb, 0xa37f60fd, 0xf701269f, 0x5c72f5bc, 0x44663bc5, 0x5bfb7e34, 0x8b432976, 0xcb23c6dc, 0xb6edfc68, 0xb8e4f163, 0xd731dcca, 0x42638510, 0x13972240, 0x84c61120, 0x854a247d, 0xd2bb3df8, 0xaef93211, 0xc729a16d, 0x1d9e2f4b, 0xdcb230f3, 0x0d8652ec, 0x77c1e3d0, 0x2bb3166c, 0xa970b999, 0x119448fa, 0x47e96422, 0xa8fc8cc4, 0xa0f03f1a, 0x567d2cd8, 0x223390ef, 0x87494ec7, 0xd938d1c1, 0x8ccaa2fe, 0x98d40b36, 0xa6f581cf, 0xa57ade28, 0xdab78e26, 0x3fadbfa4, 0x2c3a9de4, 0x5078920d, 0x6a5fcc9b, 0x547e4662, 0xf68d13c2, 0x90d8b8e8, 0x2e39f75e, 0x82c3aff5, 0x9f5d80be, 0x69d0937c, 0x6fd52da9, 0xcf2512b3, 0xc8ac993b, 0x10187da7, 0xe89c636e, 0xdb3bbb7b, 0xcd267809, 0x6e5918f4, 0xec9ab701, 0x834f9aa8, 0xe6956e65, 0xaaffe67e, 0x21bccf08, 0xef15e8e6, 0xbae79bd9, 0x4a6f36ce, 0xea9f09d4, 0x29b07cd6, 0x31a4b2af, 0x2a3f2331, 0xc6a59430, 0x35a266c0, 0x744ebc37, 0xfc82caa6, 0xe090d0b0, 0x33a7d815, 0xf104984a, 0x41ecdaf7, 0x7fcd500e, 0x1791f62f, 0x764dd68d, 0x43efb04d, 0xccaa4d54, 0xe49604df, 0x9ed1b5e3, 0x4c6a881b, 0xc12c1fb8, 0x4665517f, 0x9d5eea04, 0x018c355d, 0xfa877473, 0xfb0b412e, 0xb3671d5a, 0x92dbd252, 0xe9105633, 0x6dd64713, 0x9ad7618c, 0x37a10c7a, 0x59f8148e, 0xeb133c89, 0xcea927ee, 0xb761c935, 0xe11ce5ed, 0x7a47b13c, 0x9cd2df59, 0x55f2733f, 0x1814ce79, 0x73c737bf, 0x53f7cdea, 0x5ffdaa5b, 0xdf3d6f14, 0x7844db86, 0xcaaff381, 0xb968c43e, 0x3824342c, 0xc2a3405f, 0x161dc372, 0xbce2250c, 0x283c498b, 0xff0d9541, 0x39a80171, 0x080cb3de, 0xd8b4e49c, 0x6456c190, 0x7bcb8461, 0xd532b670, 0x486c5c74, 0xd0b85742];
var T6 = [0x5051f4a7, 0x537e4165, 0xc31a17a4, 0x963a275e, 0xcb3bab6b, 0xf11f9d45, 0xabacfa58, 0x934be303, 0x552030fa, 0xf6ad766d, 0x9188cc76, 0x25f5024c, 0xfc4fe5d7, 0xd7c52acb, 0x80263544, 0x8fb562a3, 0x49deb15a, 0x6725ba1b, 0x9845ea0e, 0xe15dfec0, 0x02c32f75, 0x12814cf0, 0xa38d4697, 0xc66bd3f9, 0xe7038f5f, 0x9515929c, 0xebbf6d7a, 0xda955259, 0x2dd4be83, 0xd3587421, 0x2949e069, 0x448ec9c8, 0x6a75c289, 0x78f48e79, 0x6b99583e, 0xdd27b971, 0xb6bee14f, 0x17f088ad, 0x66c920ac, 0xb47dce3a, 0x1863df4a, 0x82e51a31, 0x60975133, 0x4562537f, 0xe0b16477, 0x84bb6bae, 0x1cfe81a0, 0x94f9082b, 0x58704868, 0x198f45fd, 0x8794de6c, 0xb7527bf8, 0x23ab73d3, 0xe2724b02, 0x57e31f8f, 0x2a6655ab, 0x07b2eb28, 0x032fb5c2, 0x9a86c57b, 0xa5d33708, 0xf2302887, 0xb223bfa5, 0xba02036a, 0x5ced1682, 0x2b8acf1c, 0x92a779b4, 0xf0f307f2, 0xa14e69e2, 0xcd65daf4, 0xd50605be, 0x1fd13462, 0x8ac4a6fe, 0x9d342e53, 0xa0a2f355, 0x32058ae1, 0x75a4f6eb, 0x390b83ec, 0xaa4060ef, 0x065e719f, 0x51bd6e10, 0xf93e218a, 0x3d96dd06, 0xaedd3e05, 0x464de6bd, 0xb591548d, 0x0571c45d, 0x6f0406d4, 0xff605015, 0x241998fb, 0x97d6bde9, 0xcc894043, 0x7767d99e, 0xbdb0e842, 0x8807898b, 0x38e7195b, 0xdb79c8ee, 0x47a17c0a, 0xe97c420f, 0xc9f8841e, 0x00000000, 0x83098086, 0x48322bed, 0xac1e1170, 0x4e6c5a72, 0xfbfd0eff, 0x560f8538, 0x1e3daed5, 0x27362d39, 0x640a0fd9, 0x21685ca6, 0xd19b5b54, 0x3a24362e, 0xb10c0a67, 0x0f9357e7, 0xd2b4ee96, 0x9e1b9b91, 0x4f80c0c5, 0xa261dc20, 0x695a774b, 0x161c121a, 0x0ae293ba, 0xe5c0a02a, 0x433c22e0, 0x1d121b17, 0x0b0e090d, 0xadf28bc7, 0xb92db6a8, 0xc8141ea9, 0x8557f119, 0x4caf7507, 0xbbee99dd, 0xfda37f60, 0x9ff70126, 0xbc5c72f5, 0xc544663b, 0x345bfb7e, 0x768b4329, 0xdccb23c6, 0x68b6edfc, 0x63b8e4f1, 0xcad731dc, 0x10426385, 0x40139722, 0x2084c611, 0x7d854a24, 0xf8d2bb3d, 0x11aef932, 0x6dc729a1, 0x4b1d9e2f, 0xf3dcb230, 0xec0d8652, 0xd077c1e3, 0x6c2bb316, 0x99a970b9, 0xfa119448, 0x2247e964, 0xc4a8fc8c, 0x1aa0f03f, 0xd8567d2c, 0xef223390, 0xc787494e, 0xc1d938d1, 0xfe8ccaa2, 0x3698d40b, 0xcfa6f581, 0x28a57ade, 0x26dab78e, 0xa43fadbf, 0xe42c3a9d, 0x0d507892, 0x9b6a5fcc, 0x62547e46, 0xc2f68d13, 0xe890d8b8, 0x5e2e39f7, 0xf582c3af, 0xbe9f5d80, 0x7c69d093, 0xa96fd52d, 0xb3cf2512, 0x3bc8ac99, 0xa710187d, 0x6ee89c63, 0x7bdb3bbb, 0x09cd2678, 0xf46e5918, 0x01ec9ab7, 0xa8834f9a, 0x65e6956e, 0x7eaaffe6, 0x0821bccf, 0xe6ef15e8, 0xd9bae79b, 0xce4a6f36, 0xd4ea9f09, 0xd629b07c, 0xaf31a4b2, 0x312a3f23, 0x30c6a594, 0xc035a266, 0x37744ebc, 0xa6fc82ca, 0xb0e090d0, 0x1533a7d8, 0x4af10498, 0xf741ecda, 0x0e7fcd50, 0x2f1791f6, 0x8d764dd6, 0x4d43efb0, 0x54ccaa4d, 0xdfe49604, 0xe39ed1b5, 0x1b4c6a88, 0xb8c12c1f, 0x7f466551, 0x049d5eea, 0x5d018c35, 0x73fa8774, 0x2efb0b41, 0x5ab3671d, 0x5292dbd2, 0x33e91056, 0x136dd647, 0x8c9ad761, 0x7a37a10c, 0x8e59f814, 0x89eb133c, 0xeecea927, 0x35b761c9, 0xede11ce5, 0x3c7a47b1, 0x599cd2df, 0x3f55f273, 0x791814ce, 0xbf73c737, 0xea53f7cd, 0x5b5ffdaa, 0x14df3d6f, 0x867844db, 0x81caaff3, 0x3eb968c4, 0x2c382434, 0x5fc2a340, 0x72161dc3, 0x0cbce225, 0x8b283c49, 0x41ff0d95, 0x7139a801, 0xde080cb3, 0x9cd8b4e4, 0x906456c1, 0x617bcb84, 0x70d532b6, 0x74486c5c, 0x42d0b857];
var T7 = [0xa75051f4, 0x65537e41, 0xa4c31a17, 0x5e963a27, 0x6bcb3bab, 0x45f11f9d, 0x58abacfa, 0x03934be3, 0xfa552030, 0x6df6ad76, 0x769188cc, 0x4c25f502, 0xd7fc4fe5, 0xcbd7c52a, 0x44802635, 0xa38fb562, 0x5a49deb1, 0x1b6725ba, 0x0e9845ea, 0xc0e15dfe, 0x7502c32f, 0xf012814c, 0x97a38d46, 0xf9c66bd3, 0x5fe7038f, 0x9c951592, 0x7aebbf6d, 0x59da9552, 0x832dd4be, 0x21d35874, 0x692949e0, 0xc8448ec9, 0x896a75c2, 0x7978f48e, 0x3e6b9958, 0x71dd27b9, 0x4fb6bee1, 0xad17f088, 0xac66c920, 0x3ab47dce, 0x4a1863df, 0x3182e51a, 0x33609751, 0x7f456253, 0x77e0b164, 0xae84bb6b, 0xa01cfe81, 0x2b94f908, 0x68587048, 0xfd198f45, 0x6c8794de, 0xf8b7527b, 0xd323ab73, 0x02e2724b, 0x8f57e31f, 0xab2a6655, 0x2807b2eb, 0xc2032fb5, 0x7b9a86c5, 0x08a5d337, 0x87f23028, 0xa5b223bf, 0x6aba0203, 0x825ced16, 0x1c2b8acf, 0xb492a779, 0xf2f0f307, 0xe2a14e69, 0xf4cd65da, 0xbed50605, 0x621fd134, 0xfe8ac4a6, 0x539d342e, 0x55a0a2f3, 0xe132058a, 0xeb75a4f6, 0xec390b83, 0xefaa4060, 0x9f065e71, 0x1051bd6e, 0x8af93e21, 0x063d96dd, 0x05aedd3e, 0xbd464de6, 0x8db59154, 0x5d0571c4, 0xd46f0406, 0x15ff6050, 0xfb241998, 0xe997d6bd, 0x43cc8940, 0x9e7767d9, 0x42bdb0e8, 0x8b880789, 0x5b38e719, 0xeedb79c8, 0x0a47a17c, 0x0fe97c42, 0x1ec9f884, 0x00000000, 0x86830980, 0xed48322b, 0x70ac1e11, 0x724e6c5a, 0xfffbfd0e, 0x38560f85, 0xd51e3dae, 0x3927362d, 0xd9640a0f, 0xa621685c, 0x54d19b5b, 0x2e3a2436, 0x67b10c0a, 0xe70f9357, 0x96d2b4ee, 0x919e1b9b, 0xc54f80c0, 0x20a261dc, 0x4b695a77, 0x1a161c12, 0xba0ae293, 0x2ae5c0a0, 0xe0433c22, 0x171d121b, 0x0d0b0e09, 0xc7adf28b, 0xa8b92db6, 0xa9c8141e, 0x198557f1, 0x074caf75, 0xddbbee99, 0x60fda37f, 0x269ff701, 0xf5bc5c72, 0x3bc54466, 0x7e345bfb, 0x29768b43, 0xc6dccb23, 0xfc68b6ed, 0xf163b8e4, 0xdccad731, 0x85104263, 0x22401397, 0x112084c6, 0x247d854a, 0x3df8d2bb, 0x3211aef9, 0xa16dc729, 0x2f4b1d9e, 0x30f3dcb2, 0x52ec0d86, 0xe3d077c1, 0x166c2bb3, 0xb999a970, 0x48fa1194, 0x642247e9, 0x8cc4a8fc, 0x3f1aa0f0, 0x2cd8567d, 0x90ef2233, 0x4ec78749, 0xd1c1d938, 0xa2fe8cca, 0x0b3698d4, 0x81cfa6f5, 0xde28a57a, 0x8e26dab7, 0xbfa43fad, 0x9de42c3a, 0x920d5078, 0xcc9b6a5f, 0x4662547e, 0x13c2f68d, 0xb8e890d8, 0xf75e2e39, 0xaff582c3, 0x80be9f5d, 0x937c69d0, 0x2da96fd5, 0x12b3cf25, 0x993bc8ac, 0x7da71018, 0x636ee89c, 0xbb7bdb3b, 0x7809cd26, 0x18f46e59, 0xb701ec9a, 0x9aa8834f, 0x6e65e695, 0xe67eaaff, 0xcf0821bc, 0xe8e6ef15, 0x9bd9bae7, 0x36ce4a6f, 0x09d4ea9f, 0x7cd629b0, 0xb2af31a4, 0x23312a3f, 0x9430c6a5, 0x66c035a2, 0xbc37744e, 0xcaa6fc82, 0xd0b0e090, 0xd81533a7, 0x984af104, 0xdaf741ec, 0x500e7fcd, 0xf62f1791, 0xd68d764d, 0xb04d43ef, 0x4d54ccaa, 0x04dfe496, 0xb5e39ed1, 0x881b4c6a, 0x1fb8c12c, 0x517f4665, 0xea049d5e, 0x355d018c, 0x7473fa87, 0x412efb0b, 0x1d5ab367, 0xd25292db, 0x5633e910, 0x47136dd6, 0x618c9ad7, 0x0c7a37a1, 0x148e59f8, 0x3c89eb13, 0x27eecea9, 0xc935b761, 0xe5ede11c, 0xb13c7a47, 0xdf599cd2, 0x733f55f2, 0xce791814, 0x37bf73c7, 0xcdea53f7, 0xaa5b5ffd, 0x6f14df3d, 0xdb867844, 0xf381caaf, 0xc43eb968, 0x342c3824, 0x405fc2a3, 0xc372161d, 0x250cbce2, 0x498b283c, 0x9541ff0d, 0x017139a8, 0xb3de080c, 0xe49cd8b4, 0xc1906456, 0x84617bcb, 0xb670d532, 0x5c74486c, 0x5742d0b8];
var T8 = [0xf4a75051, 0x4165537e, 0x17a4c31a, 0x275e963a, 0xab6bcb3b, 0x9d45f11f, 0xfa58abac, 0xe303934b, 0x30fa5520, 0x766df6ad, 0xcc769188, 0x024c25f5, 0xe5d7fc4f, 0x2acbd7c5, 0x35448026, 0x62a38fb5, 0xb15a49de, 0xba1b6725, 0xea0e9845, 0xfec0e15d, 0x2f7502c3, 0x4cf01281, 0x4697a38d, 0xd3f9c66b, 0x8f5fe703, 0x929c9515, 0x6d7aebbf, 0x5259da95, 0xbe832dd4, 0x7421d358, 0xe0692949, 0xc9c8448e, 0xc2896a75, 0x8e7978f4, 0x583e6b99, 0xb971dd27, 0xe14fb6be, 0x88ad17f0, 0x20ac66c9, 0xce3ab47d, 0xdf4a1863, 0x1a3182e5, 0x51336097, 0x537f4562, 0x6477e0b1, 0x6bae84bb, 0x81a01cfe, 0x082b94f9, 0x48685870, 0x45fd198f, 0xde6c8794, 0x7bf8b752, 0x73d323ab, 0x4b02e272, 0x1f8f57e3, 0x55ab2a66, 0xeb2807b2, 0xb5c2032f, 0xc57b9a86, 0x3708a5d3, 0x2887f230, 0xbfa5b223, 0x036aba02, 0x16825ced, 0xcf1c2b8a, 0x79b492a7, 0x07f2f0f3, 0x69e2a14e, 0xdaf4cd65, 0x05bed506, 0x34621fd1, 0xa6fe8ac4, 0x2e539d34, 0xf355a0a2, 0x8ae13205, 0xf6eb75a4, 0x83ec390b, 0x60efaa40, 0x719f065e, 0x6e1051bd, 0x218af93e, 0xdd063d96, 0x3e05aedd, 0xe6bd464d, 0x548db591, 0xc45d0571, 0x06d46f04, 0x5015ff60, 0x98fb2419, 0xbde997d6, 0x4043cc89, 0xd99e7767, 0xe842bdb0, 0x898b8807, 0x195b38e7, 0xc8eedb79, 0x7c0a47a1, 0x420fe97c, 0x841ec9f8, 0x00000000, 0x80868309, 0x2bed4832, 0x1170ac1e, 0x5a724e6c, 0x0efffbfd, 0x8538560f, 0xaed51e3d, 0x2d392736, 0x0fd9640a, 0x5ca62168, 0x5b54d19b, 0x362e3a24, 0x0a67b10c, 0x57e70f93, 0xee96d2b4, 0x9b919e1b, 0xc0c54f80, 0xdc20a261, 0x774b695a, 0x121a161c, 0x93ba0ae2, 0xa02ae5c0, 0x22e0433c, 0x1b171d12, 0x090d0b0e, 0x8bc7adf2, 0xb6a8b92d, 0x1ea9c814, 0xf1198557, 0x75074caf, 0x99ddbbee, 0x7f60fda3, 0x01269ff7, 0x72f5bc5c, 0x663bc544, 0xfb7e345b, 0x4329768b, 0x23c6dccb, 0xedfc68b6, 0xe4f163b8, 0x31dccad7, 0x63851042, 0x97224013, 0xc6112084, 0x4a247d85, 0xbb3df8d2, 0xf93211ae, 0x29a16dc7, 0x9e2f4b1d, 0xb230f3dc, 0x8652ec0d, 0xc1e3d077, 0xb3166c2b, 0x70b999a9, 0x9448fa11, 0xe9642247, 0xfc8cc4a8, 0xf03f1aa0, 0x7d2cd856, 0x3390ef22, 0x494ec787, 0x38d1c1d9, 0xcaa2fe8c, 0xd40b3698, 0xf581cfa6, 0x7ade28a5, 0xb78e26da, 0xadbfa43f, 0x3a9de42c, 0x78920d50, 0x5fcc9b6a, 0x7e466254, 0x8d13c2f6, 0xd8b8e890, 0x39f75e2e, 0xc3aff582, 0x5d80be9f, 0xd0937c69, 0xd52da96f, 0x2512b3cf, 0xac993bc8, 0x187da710, 0x9c636ee8, 0x3bbb7bdb, 0x267809cd, 0x5918f46e, 0x9ab701ec, 0x4f9aa883, 0x956e65e6, 0xffe67eaa, 0xbccf0821, 0x15e8e6ef, 0xe79bd9ba, 0x6f36ce4a, 0x9f09d4ea, 0xb07cd629, 0xa4b2af31, 0x3f23312a, 0xa59430c6, 0xa266c035, 0x4ebc3774, 0x82caa6fc, 0x90d0b0e0, 0xa7d81533, 0x04984af1, 0xecdaf741, 0xcd500e7f, 0x91f62f17, 0x4dd68d76, 0xefb04d43, 0xaa4d54cc, 0x9604dfe4, 0xd1b5e39e, 0x6a881b4c, 0x2c1fb8c1, 0x65517f46, 0x5eea049d, 0x8c355d01, 0x877473fa, 0x0b412efb, 0x671d5ab3, 0xdbd25292, 0x105633e9, 0xd647136d, 0xd7618c9a, 0xa10c7a37, 0xf8148e59, 0x133c89eb, 0xa927eece, 0x61c935b7, 0x1ce5ede1, 0x47b13c7a, 0xd2df599c, 0xf2733f55, 0x14ce7918, 0xc737bf73, 0xf7cdea53, 0xfdaa5b5f, 0x3d6f14df, 0x44db8678, 0xaff381ca, 0x68c43eb9, 0x24342c38, 0xa3405fc2, 0x1dc37216, 0xe2250cbc, 0x3c498b28, 0x0d9541ff, 0xa8017139, 0x0cb3de08, 0xb4e49cd8, 0x56c19064, 0xcb84617b, 0x32b670d5, 0x6c5c7448, 0xb85742d0];
// Transformations for decryption key expansion
var U1 = [0x00000000, 0x0e090d0b, 0x1c121a16, 0x121b171d, 0x3824342c, 0x362d3927, 0x24362e3a, 0x2a3f2331, 0x70486858, 0x7e416553, 0x6c5a724e, 0x62537f45, 0x486c5c74, 0x4665517f, 0x547e4662, 0x5a774b69, 0xe090d0b0, 0xee99ddbb, 0xfc82caa6, 0xf28bc7ad, 0xd8b4e49c, 0xd6bde997, 0xc4a6fe8a, 0xcaaff381, 0x90d8b8e8, 0x9ed1b5e3, 0x8ccaa2fe, 0x82c3aff5, 0xa8fc8cc4, 0xa6f581cf, 0xb4ee96d2, 0xbae79bd9, 0xdb3bbb7b, 0xd532b670, 0xc729a16d, 0xc920ac66, 0xe31f8f57, 0xed16825c, 0xff0d9541, 0xf104984a, 0xab73d323, 0xa57ade28, 0xb761c935, 0xb968c43e, 0x9357e70f, 0x9d5eea04, 0x8f45fd19, 0x814cf012, 0x3bab6bcb, 0x35a266c0, 0x27b971dd, 0x29b07cd6, 0x038f5fe7, 0x0d8652ec, 0x1f9d45f1, 0x119448fa, 0x4be30393, 0x45ea0e98, 0x57f11985, 0x59f8148e, 0x73c737bf, 0x7dce3ab4, 0x6fd52da9, 0x61dc20a2, 0xad766df6, 0xa37f60fd, 0xb16477e0, 0xbf6d7aeb, 0x955259da, 0x9b5b54d1, 0x894043cc, 0x87494ec7, 0xdd3e05ae, 0xd33708a5, 0xc12c1fb8, 0xcf2512b3, 0xe51a3182, 0xeb133c89, 0xf9082b94, 0xf701269f, 0x4de6bd46, 0x43efb04d, 0x51f4a750, 0x5ffdaa5b, 0x75c2896a, 0x7bcb8461, 0x69d0937c, 0x67d99e77, 0x3daed51e, 0x33a7d815, 0x21bccf08, 0x2fb5c203, 0x058ae132, 0x0b83ec39, 0x1998fb24, 0x1791f62f, 0x764dd68d, 0x7844db86, 0x6a5fcc9b, 0x6456c190, 0x4e69e2a1, 0x4060efaa, 0x527bf8b7, 0x5c72f5bc, 0x0605bed5, 0x080cb3de, 0x1a17a4c3, 0x141ea9c8, 0x3e218af9, 0x302887f2, 0x223390ef, 0x2c3a9de4, 0x96dd063d, 0x98d40b36, 0x8acf1c2b, 0x84c61120, 0xaef93211, 0xa0f03f1a, 0xb2eb2807, 0xbce2250c, 0xe6956e65, 0xe89c636e, 0xfa877473, 0xf48e7978, 0xdeb15a49, 0xd0b85742, 0xc2a3405f, 0xccaa4d54, 0x41ecdaf7, 0x4fe5d7fc, 0x5dfec0e1, 0x53f7cdea, 0x79c8eedb, 0x77c1e3d0, 0x65daf4cd, 0x6bd3f9c6, 0x31a4b2af, 0x3fadbfa4, 0x2db6a8b9, 0x23bfa5b2, 0x09808683, 0x07898b88, 0x15929c95, 0x1b9b919e, 0xa17c0a47, 0xaf75074c, 0xbd6e1051, 0xb3671d5a, 0x99583e6b, 0x97513360, 0x854a247d, 0x8b432976, 0xd134621f, 0xdf3d6f14, 0xcd267809, 0xc32f7502, 0xe9105633, 0xe7195b38, 0xf5024c25, 0xfb0b412e, 0x9ad7618c, 0x94de6c87, 0x86c57b9a, 0x88cc7691, 0xa2f355a0, 0xacfa58ab, 0xbee14fb6, 0xb0e842bd, 0xea9f09d4, 0xe49604df, 0xf68d13c2, 0xf8841ec9, 0xd2bb3df8, 0xdcb230f3, 0xcea927ee, 0xc0a02ae5, 0x7a47b13c, 0x744ebc37, 0x6655ab2a, 0x685ca621, 0x42638510, 0x4c6a881b, 0x5e719f06, 0x5078920d, 0x0a0fd964, 0x0406d46f, 0x161dc372, 0x1814ce79, 0x322bed48, 0x3c22e043, 0x2e39f75e, 0x2030fa55, 0xec9ab701, 0xe293ba0a, 0xf088ad17, 0xfe81a01c, 0xd4be832d, 0xdab78e26, 0xc8ac993b, 0xc6a59430, 0x9cd2df59, 0x92dbd252, 0x80c0c54f, 0x8ec9c844, 0xa4f6eb75, 0xaaffe67e, 0xb8e4f163, 0xb6edfc68, 0x0c0a67b1, 0x02036aba, 0x10187da7, 0x1e1170ac, 0x342e539d, 0x3a275e96, 0x283c498b, 0x26354480, 0x7c420fe9, 0x724b02e2, 0x605015ff, 0x6e5918f4, 0x44663bc5, 0x4a6f36ce, 0x587421d3, 0x567d2cd8, 0x37a10c7a, 0x39a80171, 0x2bb3166c, 0x25ba1b67, 0x0f853856, 0x018c355d, 0x13972240, 0x1d9e2f4b, 0x47e96422, 0x49e06929, 0x5bfb7e34, 0x55f2733f, 0x7fcd500e, 0x71c45d05, 0x63df4a18, 0x6dd64713, 0xd731dcca, 0xd938d1c1, 0xcb23c6dc, 0xc52acbd7, 0xef15e8e6, 0xe11ce5ed, 0xf307f2f0, 0xfd0efffb, 0xa779b492, 0xa970b999, 0xbb6bae84, 0xb562a38f, 0x9f5d80be, 0x91548db5, 0x834f9aa8, 0x8d4697a3];
var U2 = [0x00000000, 0x0b0e090d, 0x161c121a, 0x1d121b17, 0x2c382434, 0x27362d39, 0x3a24362e, 0x312a3f23, 0x58704868, 0x537e4165, 0x4e6c5a72, 0x4562537f, 0x74486c5c, 0x7f466551, 0x62547e46, 0x695a774b, 0xb0e090d0, 0xbbee99dd, 0xa6fc82ca, 0xadf28bc7, 0x9cd8b4e4, 0x97d6bde9, 0x8ac4a6fe, 0x81caaff3, 0xe890d8b8, 0xe39ed1b5, 0xfe8ccaa2, 0xf582c3af, 0xc4a8fc8c, 0xcfa6f581, 0xd2b4ee96, 0xd9bae79b, 0x7bdb3bbb, 0x70d532b6, 0x6dc729a1, 0x66c920ac, 0x57e31f8f, 0x5ced1682, 0x41ff0d95, 0x4af10498, 0x23ab73d3, 0x28a57ade, 0x35b761c9, 0x3eb968c4, 0x0f9357e7, 0x049d5eea, 0x198f45fd, 0x12814cf0, 0xcb3bab6b, 0xc035a266, 0xdd27b971, 0xd629b07c, 0xe7038f5f, 0xec0d8652, 0xf11f9d45, 0xfa119448, 0x934be303, 0x9845ea0e, 0x8557f119, 0x8e59f814, 0xbf73c737, 0xb47dce3a, 0xa96fd52d, 0xa261dc20, 0xf6ad766d, 0xfda37f60, 0xe0b16477, 0xebbf6d7a, 0xda955259, 0xd19b5b54, 0xcc894043, 0xc787494e, 0xaedd3e05, 0xa5d33708, 0xb8c12c1f, 0xb3cf2512, 0x82e51a31, 0x89eb133c, 0x94f9082b, 0x9ff70126, 0x464de6bd, 0x4d43efb0, 0x5051f4a7, 0x5b5ffdaa, 0x6a75c289, 0x617bcb84, 0x7c69d093, 0x7767d99e, 0x1e3daed5, 0x1533a7d8, 0x0821bccf, 0x032fb5c2, 0x32058ae1, 0x390b83ec, 0x241998fb, 0x2f1791f6, 0x8d764dd6, 0x867844db, 0x9b6a5fcc, 0x906456c1, 0xa14e69e2, 0xaa4060ef, 0xb7527bf8, 0xbc5c72f5, 0xd50605be, 0xde080cb3, 0xc31a17a4, 0xc8141ea9, 0xf93e218a, 0xf2302887, 0xef223390, 0xe42c3a9d, 0x3d96dd06, 0x3698d40b, 0x2b8acf1c, 0x2084c611, 0x11aef932, 0x1aa0f03f, 0x07b2eb28, 0x0cbce225, 0x65e6956e, 0x6ee89c63, 0x73fa8774, 0x78f48e79, 0x49deb15a, 0x42d0b857, 0x5fc2a340, 0x54ccaa4d, 0xf741ecda, 0xfc4fe5d7, 0xe15dfec0, 0xea53f7cd, 0xdb79c8ee, 0xd077c1e3, 0xcd65daf4, 0xc66bd3f9, 0xaf31a4b2, 0xa43fadbf, 0xb92db6a8, 0xb223bfa5, 0x83098086, 0x8807898b, 0x9515929c, 0x9e1b9b91, 0x47a17c0a, 0x4caf7507, 0x51bd6e10, 0x5ab3671d, 0x6b99583e, 0x60975133, 0x7d854a24, 0x768b4329, 0x1fd13462, 0x14df3d6f, 0x09cd2678, 0x02c32f75, 0x33e91056, 0x38e7195b, 0x25f5024c, 0x2efb0b41, 0x8c9ad761, 0x8794de6c, 0x9a86c57b, 0x9188cc76, 0xa0a2f355, 0xabacfa58, 0xb6bee14f, 0xbdb0e842, 0xd4ea9f09, 0xdfe49604, 0xc2f68d13, 0xc9f8841e, 0xf8d2bb3d, 0xf3dcb230, 0xeecea927, 0xe5c0a02a, 0x3c7a47b1, 0x37744ebc, 0x2a6655ab, 0x21685ca6, 0x10426385, 0x1b4c6a88, 0x065e719f, 0x0d507892, 0x640a0fd9, 0x6f0406d4, 0x72161dc3, 0x791814ce, 0x48322bed, 0x433c22e0, 0x5e2e39f7, 0x552030fa, 0x01ec9ab7, 0x0ae293ba, 0x17f088ad, 0x1cfe81a0, 0x2dd4be83, 0x26dab78e, 0x3bc8ac99, 0x30c6a594, 0x599cd2df, 0x5292dbd2, 0x4f80c0c5, 0x448ec9c8, 0x75a4f6eb, 0x7eaaffe6, 0x63b8e4f1, 0x68b6edfc, 0xb10c0a67, 0xba02036a, 0xa710187d, 0xac1e1170, 0x9d342e53, 0x963a275e, 0x8b283c49, 0x80263544, 0xe97c420f, 0xe2724b02, 0xff605015, 0xf46e5918, 0xc544663b, 0xce4a6f36, 0xd3587421, 0xd8567d2c, 0x7a37a10c, 0x7139a801, 0x6c2bb316, 0x6725ba1b, 0x560f8538, 0x5d018c35, 0x40139722, 0x4b1d9e2f, 0x2247e964, 0x2949e069, 0x345bfb7e, 0x3f55f273, 0x0e7fcd50, 0x0571c45d, 0x1863df4a, 0x136dd647, 0xcad731dc, 0xc1d938d1, 0xdccb23c6, 0xd7c52acb, 0xe6ef15e8, 0xede11ce5, 0xf0f307f2, 0xfbfd0eff, 0x92a779b4, 0x99a970b9, 0x84bb6bae, 0x8fb562a3, 0xbe9f5d80, 0xb591548d, 0xa8834f9a, 0xa38d4697];
var U3 = [0x00000000, 0x0d0b0e09, 0x1a161c12, 0x171d121b, 0x342c3824, 0x3927362d, 0x2e3a2436, 0x23312a3f, 0x68587048, 0x65537e41, 0x724e6c5a, 0x7f456253, 0x5c74486c, 0x517f4665, 0x4662547e, 0x4b695a77, 0xd0b0e090, 0xddbbee99, 0xcaa6fc82, 0xc7adf28b, 0xe49cd8b4, 0xe997d6bd, 0xfe8ac4a6, 0xf381caaf, 0xb8e890d8, 0xb5e39ed1, 0xa2fe8cca, 0xaff582c3, 0x8cc4a8fc, 0x81cfa6f5, 0x96d2b4ee, 0x9bd9bae7, 0xbb7bdb3b, 0xb670d532, 0xa16dc729, 0xac66c920, 0x8f57e31f, 0x825ced16, 0x9541ff0d, 0x984af104, 0xd323ab73, 0xde28a57a, 0xc935b761, 0xc43eb968, 0xe70f9357, 0xea049d5e, 0xfd198f45, 0xf012814c, 0x6bcb3bab, 0x66c035a2, 0x71dd27b9, 0x7cd629b0, 0x5fe7038f, 0x52ec0d86, 0x45f11f9d, 0x48fa1194, 0x03934be3, 0x0e9845ea, 0x198557f1, 0x148e59f8, 0x37bf73c7, 0x3ab47dce, 0x2da96fd5, 0x20a261dc, 0x6df6ad76, 0x60fda37f, 0x77e0b164, 0x7aebbf6d, 0x59da9552, 0x54d19b5b, 0x43cc8940, 0x4ec78749, 0x05aedd3e, 0x08a5d337, 0x1fb8c12c, 0x12b3cf25, 0x3182e51a, 0x3c89eb13, 0x2b94f908, 0x269ff701, 0xbd464de6, 0xb04d43ef, 0xa75051f4, 0xaa5b5ffd, 0x896a75c2, 0x84617bcb, 0x937c69d0, 0x9e7767d9, 0xd51e3dae, 0xd81533a7, 0xcf0821bc, 0xc2032fb5, 0xe132058a, 0xec390b83, 0xfb241998, 0xf62f1791, 0xd68d764d, 0xdb867844, 0xcc9b6a5f, 0xc1906456, 0xe2a14e69, 0xefaa4060, 0xf8b7527b, 0xf5bc5c72, 0xbed50605, 0xb3de080c, 0xa4c31a17, 0xa9c8141e, 0x8af93e21, 0x87f23028, 0x90ef2233, 0x9de42c3a, 0x063d96dd, 0x0b3698d4, 0x1c2b8acf, 0x112084c6, 0x3211aef9, 0x3f1aa0f0, 0x2807b2eb, 0x250cbce2, 0x6e65e695, 0x636ee89c, 0x7473fa87, 0x7978f48e, 0x5a49deb1, 0x5742d0b8, 0x405fc2a3, 0x4d54ccaa, 0xdaf741ec, 0xd7fc4fe5, 0xc0e15dfe, 0xcdea53f7, 0xeedb79c8, 0xe3d077c1, 0xf4cd65da, 0xf9c66bd3, 0xb2af31a4, 0xbfa43fad, 0xa8b92db6, 0xa5b223bf, 0x86830980, 0x8b880789, 0x9c951592, 0x919e1b9b, 0x0a47a17c, 0x074caf75, 0x1051bd6e, 0x1d5ab367, 0x3e6b9958, 0x33609751, 0x247d854a, 0x29768b43, 0x621fd134, 0x6f14df3d, 0x7809cd26, 0x7502c32f, 0x5633e910, 0x5b38e719, 0x4c25f502, 0x412efb0b, 0x618c9ad7, 0x6c8794de, 0x7b9a86c5, 0x769188cc, 0x55a0a2f3, 0x58abacfa, 0x4fb6bee1, 0x42bdb0e8, 0x09d4ea9f, 0x04dfe496, 0x13c2f68d, 0x1ec9f884, 0x3df8d2bb, 0x30f3dcb2, 0x27eecea9, 0x2ae5c0a0, 0xb13c7a47, 0xbc37744e, 0xab2a6655, 0xa621685c, 0x85104263, 0x881b4c6a, 0x9f065e71, 0x920d5078, 0xd9640a0f, 0xd46f0406, 0xc372161d, 0xce791814, 0xed48322b, 0xe0433c22, 0xf75e2e39, 0xfa552030, 0xb701ec9a, 0xba0ae293, 0xad17f088, 0xa01cfe81, 0x832dd4be, 0x8e26dab7, 0x993bc8ac, 0x9430c6a5, 0xdf599cd2, 0xd25292db, 0xc54f80c0, 0xc8448ec9, 0xeb75a4f6, 0xe67eaaff, 0xf163b8e4, 0xfc68b6ed, 0x67b10c0a, 0x6aba0203, 0x7da71018, 0x70ac1e11, 0x539d342e, 0x5e963a27, 0x498b283c, 0x44802635, 0x0fe97c42, 0x02e2724b, 0x15ff6050, 0x18f46e59, 0x3bc54466, 0x36ce4a6f, 0x21d35874, 0x2cd8567d, 0x0c7a37a1, 0x017139a8, 0x166c2bb3, 0x1b6725ba, 0x38560f85, 0x355d018c, 0x22401397, 0x2f4b1d9e, 0x642247e9, 0x692949e0, 0x7e345bfb, 0x733f55f2, 0x500e7fcd, 0x5d0571c4, 0x4a1863df, 0x47136dd6, 0xdccad731, 0xd1c1d938, 0xc6dccb23, 0xcbd7c52a, 0xe8e6ef15, 0xe5ede11c, 0xf2f0f307, 0xfffbfd0e, 0xb492a779, 0xb999a970, 0xae84bb6b, 0xa38fb562, 0x80be9f5d, 0x8db59154, 0x9aa8834f, 0x97a38d46];
var U4 = [0x00000000, 0x090d0b0e, 0x121a161c, 0x1b171d12, 0x24342c38, 0x2d392736, 0x362e3a24, 0x3f23312a, 0x48685870, 0x4165537e, 0x5a724e6c, 0x537f4562, 0x6c5c7448, 0x65517f46, 0x7e466254, 0x774b695a, 0x90d0b0e0, 0x99ddbbee, 0x82caa6fc, 0x8bc7adf2, 0xb4e49cd8, 0xbde997d6, 0xa6fe8ac4, 0xaff381ca, 0xd8b8e890, 0xd1b5e39e, 0xcaa2fe8c, 0xc3aff582, 0xfc8cc4a8, 0xf581cfa6, 0xee96d2b4, 0xe79bd9ba, 0x3bbb7bdb, 0x32b670d5, 0x29a16dc7, 0x20ac66c9, 0x1f8f57e3, 0x16825ced, 0x0d9541ff, 0x04984af1, 0x73d323ab, 0x7ade28a5, 0x61c935b7, 0x68c43eb9, 0x57e70f93, 0x5eea049d, 0x45fd198f, 0x4cf01281, 0xab6bcb3b, 0xa266c035, 0xb971dd27, 0xb07cd629, 0x8f5fe703, 0x8652ec0d, 0x9d45f11f, 0x9448fa11, 0xe303934b, 0xea0e9845, 0xf1198557, 0xf8148e59, 0xc737bf73, 0xce3ab47d, 0xd52da96f, 0xdc20a261, 0x766df6ad, 0x7f60fda3, 0x6477e0b1, 0x6d7aebbf, 0x5259da95, 0x5b54d19b, 0x4043cc89, 0x494ec787, 0x3e05aedd, 0x3708a5d3, 0x2c1fb8c1, 0x2512b3cf, 0x1a3182e5, 0x133c89eb, 0x082b94f9, 0x01269ff7, 0xe6bd464d, 0xefb04d43, 0xf4a75051, 0xfdaa5b5f, 0xc2896a75, 0xcb84617b, 0xd0937c69, 0xd99e7767, 0xaed51e3d, 0xa7d81533, 0xbccf0821, 0xb5c2032f, 0x8ae13205, 0x83ec390b, 0x98fb2419, 0x91f62f17, 0x4dd68d76, 0x44db8678, 0x5fcc9b6a, 0x56c19064, 0x69e2a14e, 0x60efaa40, 0x7bf8b752, 0x72f5bc5c, 0x05bed506, 0x0cb3de08, 0x17a4c31a, 0x1ea9c814, 0x218af93e, 0x2887f230, 0x3390ef22, 0x3a9de42c, 0xdd063d96, 0xd40b3698, 0xcf1c2b8a, 0xc6112084, 0xf93211ae, 0xf03f1aa0, 0xeb2807b2, 0xe2250cbc, 0x956e65e6, 0x9c636ee8, 0x877473fa, 0x8e7978f4, 0xb15a49de, 0xb85742d0, 0xa3405fc2, 0xaa4d54cc, 0xecdaf741, 0xe5d7fc4f, 0xfec0e15d, 0xf7cdea53, 0xc8eedb79, 0xc1e3d077, 0xdaf4cd65, 0xd3f9c66b, 0xa4b2af31, 0xadbfa43f, 0xb6a8b92d, 0xbfa5b223, 0x80868309, 0x898b8807, 0x929c9515, 0x9b919e1b, 0x7c0a47a1, 0x75074caf, 0x6e1051bd, 0x671d5ab3, 0x583e6b99, 0x51336097, 0x4a247d85, 0x4329768b, 0x34621fd1, 0x3d6f14df, 0x267809cd, 0x2f7502c3, 0x105633e9, 0x195b38e7, 0x024c25f5, 0x0b412efb, 0xd7618c9a, 0xde6c8794, 0xc57b9a86, 0xcc769188, 0xf355a0a2, 0xfa58abac, 0xe14fb6be, 0xe842bdb0, 0x9f09d4ea, 0x9604dfe4, 0x8d13c2f6, 0x841ec9f8, 0xbb3df8d2, 0xb230f3dc, 0xa927eece, 0xa02ae5c0, 0x47b13c7a, 0x4ebc3774, 0x55ab2a66, 0x5ca62168, 0x63851042, 0x6a881b4c, 0x719f065e, 0x78920d50, 0x0fd9640a, 0x06d46f04, 0x1dc37216, 0x14ce7918, 0x2bed4832, 0x22e0433c, 0x39f75e2e, 0x30fa5520, 0x9ab701ec, 0x93ba0ae2, 0x88ad17f0, 0x81a01cfe, 0xbe832dd4, 0xb78e26da, 0xac993bc8, 0xa59430c6, 0xd2df599c, 0xdbd25292, 0xc0c54f80, 0xc9c8448e, 0xf6eb75a4, 0xffe67eaa, 0xe4f163b8, 0xedfc68b6, 0x0a67b10c, 0x036aba02, 0x187da710, 0x1170ac1e, 0x2e539d34, 0x275e963a, 0x3c498b28, 0x35448026, 0x420fe97c, 0x4b02e272, 0x5015ff60, 0x5918f46e, 0x663bc544, 0x6f36ce4a, 0x7421d358, 0x7d2cd856, 0xa10c7a37, 0xa8017139, 0xb3166c2b, 0xba1b6725, 0x8538560f, 0x8c355d01, 0x97224013, 0x9e2f4b1d, 0xe9642247, 0xe0692949, 0xfb7e345b, 0xf2733f55, 0xcd500e7f, 0xc45d0571, 0xdf4a1863, 0xd647136d, 0x31dccad7, 0x38d1c1d9, 0x23c6dccb, 0x2acbd7c5, 0x15e8e6ef, 0x1ce5ede1, 0x07f2f0f3, 0x0efffbfd, 0x79b492a7, 0x70b999a9, 0x6bae84bb, 0x62a38fb5, 0x5d80be9f, 0x548db591, 0x4f9aa883, 0x4697a38d];
function convertToInt32(bytes) {
var result = [];
for (var i = 0; i < bytes.length; i += 4) {
result.push(
(bytes[i ] << 24) |
(bytes[i + 1] << 16) |
(bytes[i + 2] << 8) |
bytes[i + 3]
);
}
return result;
}
var AES = function(key) {
if (!(this instanceof AES)) {
throw Error('AES must be instanitated with `new`');
}
Object.defineProperty(this, 'key', {
value: coerceArray(key, true)
});
this._prepare();
}
AES.prototype._prepare = function() {
var rounds = numberOfRounds[this.key.length];
if (rounds == null) {
throw new Error('invalid key size (must be 16, 24 or 32 bytes)');
}
// encryption round keys
this._Ke = [];
// decryption round keys
this._Kd = [];
for (var i = 0; i <= rounds; i++) {
this._Ke.push([0, 0, 0, 0]);
this._Kd.push([0, 0, 0, 0]);
}
var roundKeyCount = (rounds + 1) * 4;
var KC = this.key.length / 4;
// convert the key into ints
var tk = convertToInt32(this.key);
// copy values into round key arrays
var index;
for (var i = 0; i < KC; i++) {
index = i >> 2;
this._Ke[index][i % 4] = tk[i];
this._Kd[rounds - index][i % 4] = tk[i];
}
// key expansion (fips-197 section 5.2)
var rconpointer = 0;
var t = KC, tt;
while (t < roundKeyCount) {
tt = tk[KC - 1];
tk[0] ^= ((S[(tt >> 16) & 0xFF] << 24) ^
(S[(tt >> 8) & 0xFF] << 16) ^
(S[ tt & 0xFF] << 8) ^
S[(tt >> 24) & 0xFF] ^
(rcon[rconpointer] << 24));
rconpointer += 1;
// key expansion (for non-256 bit)
if (KC != 8) {
for (var i = 1; i < KC; i++) {
tk[i] ^= tk[i - 1];
}
// key expansion for 256-bit keys is "slightly different" (fips-197)
} else {
for (var i = 1; i < (KC / 2); i++) {
tk[i] ^= tk[i - 1];
}
tt = tk[(KC / 2) - 1];
tk[KC / 2] ^= (S[ tt & 0xFF] ^
(S[(tt >> 8) & 0xFF] << 8) ^
(S[(tt >> 16) & 0xFF] << 16) ^
(S[(tt >> 24) & 0xFF] << 24));
for (var i = (KC / 2) + 1; i < KC; i++) {
tk[i] ^= tk[i - 1];
}
}
// copy values into round key arrays
var i = 0, r, c;
while (i < KC && t < roundKeyCount) {
r = t >> 2;
c = t % 4;
this._Ke[r][c] = tk[i];
this._Kd[rounds - r][c] = tk[i++];
t++;
}
}
// inverse-cipher-ify the decryption round key (fips-197 section 5.3)
for (var r = 1; r < rounds; r++) {
for (var c = 0; c < 4; c++) {
tt = this._Kd[r][c];
this._Kd[r][c] = (U1[(tt >> 24) & 0xFF] ^
U2[(tt >> 16) & 0xFF] ^
U3[(tt >> 8) & 0xFF] ^
U4[ tt & 0xFF]);
}
}
}
AES.prototype.encrypt = function(plaintext) {
if (plaintext.length != 16) {
throw new Error('invalid plaintext size (must be 16 bytes)');
}
var rounds = this._Ke.length - 1;
var a = [0, 0, 0, 0];
// convert plaintext to (ints ^ key)
var t = convertToInt32(plaintext);
for (var i = 0; i < 4; i++) {
t[i] ^= this._Ke[0][i];
}
// apply round transforms
for (var r = 1; r < rounds; r++) {
for (var i = 0; i < 4; i++) {
a[i] = (T1[(t[ i ] >> 24) & 0xff] ^
T2[(t[(i + 1) % 4] >> 16) & 0xff] ^
T3[(t[(i + 2) % 4] >> 8) & 0xff] ^
T4[ t[(i + 3) % 4] & 0xff] ^
this._Ke[r][i]);
}
t = a.slice();
}
// the last round is special
var result = createArray(16), tt;
for (var i = 0; i < 4; i++) {
tt = this._Ke[rounds][i];
result[4 * i ] = (S[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
result[4 * i + 1] = (S[(t[(i + 1) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
result[4 * i + 2] = (S[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
result[4 * i + 3] = (S[ t[(i + 3) % 4] & 0xff] ^ tt ) & 0xff;
}
return result;
}
AES.prototype.decrypt = function(ciphertext) {
if (ciphertext.length != 16) {
throw new Error('invalid ciphertext size (must be 16 bytes)');
}
var rounds = this._Kd.length - 1;
var a = [0, 0, 0, 0];
// convert plaintext to (ints ^ key)
var t = convertToInt32(ciphertext);
for (var i = 0; i < 4; i++) {
t[i] ^= this._Kd[0][i];
}
// apply round transforms
for (var r = 1; r < rounds; r++) {
for (var i = 0; i < 4; i++) {
a[i] = (T5[(t[ i ] >> 24) & 0xff] ^
T6[(t[(i + 3) % 4] >> 16) & 0xff] ^
T7[(t[(i + 2) % 4] >> 8) & 0xff] ^
T8[ t[(i + 1) % 4] & 0xff] ^
this._Kd[r][i]);
}
t = a.slice();
}
// the last round is special
var result = createArray(16), tt;
for (var i = 0; i < 4; i++) {
tt = this._Kd[rounds][i];
result[4 * i ] = (Si[(t[ i ] >> 24) & 0xff] ^ (tt >> 24)) & 0xff;
result[4 * i + 1] = (Si[(t[(i + 3) % 4] >> 16) & 0xff] ^ (tt >> 16)) & 0xff;
result[4 * i + 2] = (Si[(t[(i + 2) % 4] >> 8) & 0xff] ^ (tt >> 8)) & 0xff;
result[4 * i + 3] = (Si[ t[(i + 1) % 4] & 0xff] ^ tt ) & 0xff;
}
return result;
}
/**
* Mode Of Operation - Electonic Codebook (ECB)
*/
var ModeOfOperationECB = function(key) {
if (!(this instanceof ModeOfOperationECB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Electronic Code Block";
this.name = "ecb";
this._aes = new AES(key);
}
ModeOfOperationECB.prototype.encrypt = function(plaintext) {
plaintext = coerceArray(plaintext);
if ((plaintext.length % 16) !== 0) {
throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
}
var ciphertext = createArray(plaintext.length);
var block = createArray(16);
for (var i = 0; i < plaintext.length; i += 16) {
copyArray(plaintext, block, 0, i, i + 16);
block = this._aes.encrypt(block);
copyArray(block, ciphertext, i);
}
return ciphertext;
}
ModeOfOperationECB.prototype.decrypt = function(ciphertext) {
ciphertext = coerceArray(ciphertext);
if ((ciphertext.length % 16) !== 0) {
throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
}
var plaintext = createArray(ciphertext.length);
var block = createArray(16);
for (var i = 0; i < ciphertext.length; i += 16) {
copyArray(ciphertext, block, 0, i, i + 16);
block = this._aes.decrypt(block);
copyArray(block, plaintext, i);
}
return plaintext;
}
/**
* Mode Of Operation - Cipher Block Chaining (CBC)
*/
var ModeOfOperationCBC = function(key, iv) {
if (!(this instanceof ModeOfOperationCBC)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Cipher Block Chaining";
this.name = "cbc";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 bytes)');
}
this._lastCipherblock = coerceArray(iv, true);
this._aes = new AES(key);
}
ModeOfOperationCBC.prototype.encrypt = function(plaintext) {
plaintext = coerceArray(plaintext);
if ((plaintext.length % 16) !== 0) {
throw new Error('invalid plaintext size (must be multiple of 16 bytes)');
}
var ciphertext = createArray(plaintext.length);
var block = createArray(16);
for (var i = 0; i < plaintext.length; i += 16) {
copyArray(plaintext, block, 0, i, i + 16);
for (var j = 0; j < 16; j++) {
block[j] ^= this._lastCipherblock[j];
}
this._lastCipherblock = this._aes.encrypt(block);
copyArray(this._lastCipherblock, ciphertext, i);
}
return ciphertext;
}
ModeOfOperationCBC.prototype.decrypt = function(ciphertext) {
ciphertext = coerceArray(ciphertext);
if ((ciphertext.length % 16) !== 0) {
throw new Error('invalid ciphertext size (must be multiple of 16 bytes)');
}
var plaintext = createArray(ciphertext.length);
var block = createArray(16);
for (var i = 0; i < ciphertext.length; i += 16) {
copyArray(ciphertext, block, 0, i, i + 16);
block = this._aes.decrypt(block);
for (var j = 0; j < 16; j++) {
plaintext[i + j] = block[j] ^ this._lastCipherblock[j];
}
copyArray(ciphertext, this._lastCipherblock, 0, i, i + 16);
}
return plaintext;
}
/**
* Mode Of Operation - Cipher Feedback (CFB)
*/
var ModeOfOperationCFB = function(key, iv, segmentSize) {
if (!(this instanceof ModeOfOperationCFB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Cipher Feedback";
this.name = "cfb";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 size)');
}
if (!segmentSize) { segmentSize = 1; }
this.segmentSize = segmentSize;
this._shiftRegister = coerceArray(iv, true);
this._aes = new AES(key);
}
ModeOfOperationCFB.prototype.encrypt = function(plaintext) {
if ((plaintext.length % this.segmentSize) != 0) {
throw new Error('invalid plaintext size (must be segmentSize bytes)');
}
var encrypted = coerceArray(plaintext, true);
var xorSegment;
for (var i = 0; i < encrypted.length; i += this.segmentSize) {
xorSegment = this._aes.encrypt(this._shiftRegister);
for (var j = 0; j < this.segmentSize; j++) {
encrypted[i + j] ^= xorSegment[j];
}
// Shift the register
copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
copyArray(encrypted, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
}
return encrypted;
}
ModeOfOperationCFB.prototype.decrypt = function(ciphertext) {
if ((ciphertext.length % this.segmentSize) != 0) {
throw new Error('invalid ciphertext size (must be segmentSize bytes)');
}
var plaintext = coerceArray(ciphertext, true);
var xorSegment;
for (var i = 0; i < plaintext.length; i += this.segmentSize) {
xorSegment = this._aes.encrypt(this._shiftRegister);
for (var j = 0; j < this.segmentSize; j++) {
plaintext[i + j] ^= xorSegment[j];
}
// Shift the register
copyArray(this._shiftRegister, this._shiftRegister, 0, this.segmentSize);
copyArray(ciphertext, this._shiftRegister, 16 - this.segmentSize, i, i + this.segmentSize);
}
return plaintext;
}
/**
* Mode Of Operation - Output Feedback (OFB)
*/
var ModeOfOperationOFB = function(key, iv) {
if (!(this instanceof ModeOfOperationOFB)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Output Feedback";
this.name = "ofb";
if (!iv) {
iv = createArray(16);
} else if (iv.length != 16) {
throw new Error('invalid initialation vector size (must be 16 bytes)');
}
this._lastPrecipher = coerceArray(iv, true);
this._lastPrecipherIndex = 16;
this._aes = new AES(key);
}
ModeOfOperationOFB.prototype.encrypt = function(plaintext) {
var encrypted = coerceArray(plaintext, true);
for (var i = 0; i < encrypted.length; i++) {
if (this._lastPrecipherIndex === 16) {
this._lastPrecipher = this._aes.encrypt(this._lastPrecipher);
this._lastPrecipherIndex = 0;
}
encrypted[i] ^= this._lastPrecipher[this._lastPrecipherIndex++];
}
return encrypted;
}
// Decryption is symetric
ModeOfOperationOFB.prototype.decrypt = ModeOfOperationOFB.prototype.encrypt;
/**
* Counter object for CTR common mode of operation
*/
var Counter = function(initialValue) {
if (!(this instanceof Counter)) {
throw Error('Counter must be instanitated with `new`');
}
// We allow 0, but anything false-ish uses the default 1
if (initialValue !== 0 && !initialValue) { initialValue = 1; }
if (typeof(initialValue) === 'number') {
this._counter = createArray(16);
this.setValue(initialValue);
} else {
this.setBytes(initialValue);
}
}
Counter.prototype.setValue = function(value) {
if (typeof(value) !== 'number' || parseInt(value) != value) {
throw new Error('invalid counter value (must be an integer)');
}
// We cannot safely handle numbers beyond the safe range for integers
if (value > Number.MAX_SAFE_INTEGER) {
throw new Error('integer value out of safe range');
}
for (var index = 15; index >= 0; --index) {
this._counter[index] = value % 256;
value = parseInt(value / 256);
}
}
Counter.prototype.setBytes = function(bytes) {
bytes = coerceArray(bytes, true);
if (bytes.length != 16) {
throw new Error('invalid counter bytes size (must be 16 bytes)');
}
this._counter = bytes;
};
Counter.prototype.increment = function() {
for (var i = 15; i >= 0; i--) {
if (this._counter[i] === 255) {
this._counter[i] = 0;
} else {
this._counter[i]++;
break;
}
}
}
/**
* Mode Of Operation - Counter (CTR)
*/
var ModeOfOperationCTR = function(key, counter) {
if (!(this instanceof ModeOfOperationCTR)) {
throw Error('AES must be instanitated with `new`');
}
this.description = "Counter";
this.name = "ctr";
if (!(counter instanceof Counter)) {
counter = new Counter(counter)
}
this._counter = counter;
this._remainingCounter = null;
this._remainingCounterIndex = 16;
this._aes = new AES(key);
}
ModeOfOperationCTR.prototype.encrypt = function(plaintext) {
var encrypted = coerceArray(plaintext, true);
for (var i = 0; i < encrypted.length; i++) {
if (this._remainingCounterIndex === 16) {
this._remainingCounter = this._aes.encrypt(this._counter._counter);
this._remainingCounterIndex = 0;
this._counter.increment();
}
encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];
}
return encrypted;
}
// Decryption is symetric
ModeOfOperationCTR.prototype.decrypt = ModeOfOperationCTR.prototype.encrypt;
///////////////////////
// Padding
// See:https://tools.ietf.org/html/rfc2315
function pkcs7pad(data) {
data = coerceArray(data, true);
var padder = 16 - (data.length % 16);
var result = createArray(data.length + padder);
copyArray(data, result);
for (var i = data.length; i < result.length; i++) {
result[i] = padder;
}
return result;
}
function pkcs7strip(data) {
data = coerceArray(data, true);
if (data.length < 16) { throw new Error('PKCS#7 invalid length'); }
var padder = data[data.length - 1];
if (padder > 16) { throw new Error('PKCS#7 padding byte out of range'); }
var length = data.length - padder;
for (var i = 0; i < padder; i++) {
if (data[length + i] !== padder) {
throw new Error('PKCS#7 invalid padding byte');
}
}
var result = createArray(length);
copyArray(data, result, 0, 0, length);
return result;
}
///////////////////////
// Exporting
// The block cipher
var aesjs = {
AES: AES,
Counter: Counter,
ModeOfOperation: {
ecb: ModeOfOperationECB,
cbc: ModeOfOperationCBC,
cfb: ModeOfOperationCFB,
ofb: ModeOfOperationOFB,
ctr: ModeOfOperationCTR
},
utils: {
hex: convertHex,
utf8: convertUtf8
},
padding: {
pkcs7: {
pad: pkcs7pad,
strip: pkcs7strip
}
},
_arrayTest: {
coerceArray: coerceArray,
createArray: createArray,
copyArray: copyArray,
}
};
// node.js
if (typeof exports !== 'undefined') {
module.exports = aesjs
// RequireJS/AMD
// http://www.requirejs.org/docs/api.html
// https://github.com/amdjs/amdjs-api/wiki/AMD
} else if (typeof(define) === 'function' && define.amd) {
define([], function() { return aesjs; });
// Web Browsers
} else {
// If there was an existing library at "aesjs" make sure it's still available
if (root.aesjs) {
aesjs._aesjs = root.aesjs;
}
root.aesjs = aesjs;
}
})(this);

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2006, Ivan Sagalaev.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,97 @@
/*!
Theme: Default Description: Original highlight.js style Author: (c) Ivan
Sagalaev <maniac@softwaremaniacs.org> Maintainer: @highlightjs/core-team
Website: https://highlightjs.org/ License: see project LICENSE Touched: 2021
*/
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
}
code.hljs {
padding: 3px 5px
}
.hljs {
background: #f3f3f3;
color: #444
}
.hljs-comment {
color: #697070
}
.hljs-punctuation,
.hljs-tag {
color: #444a
}
.hljs-tag .hljs-attr,
.hljs-tag .hljs-name {
color: #444
}
.hljs-attribute,
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-name,
.hljs-selector-tag {
font-weight: 700
}
.hljs-deletion,
.hljs-number,
.hljs-quote,
.hljs-selector-class,
.hljs-selector-id,
.hljs-string,
.hljs-template-tag,
.hljs-type {
color: #800
}
.hljs-section,
.hljs-title {
color: #800;
font-weight: 700
}
.hljs-link,
.hljs-operator,
.hljs-regexp,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-symbol,
.hljs-template-variable,
.hljs-variable {
color: #ab5656
}
.hljs-literal {
color: #695
}
.hljs-addition,
.hljs-built_in,
.hljs-bullet,
.hljs-code {
color: #397300
}
.hljs-meta {
color: #1f7199
}
.hljs-meta .hljs-string {
color: #38a
}
.hljs-emphasis {
font-style: italic
}
.hljs-strong {
font-weight: 700
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
templates/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

1392
templates/assets/water.css Normal file

File diff suppressed because it is too large Load diff

28
templates/auth_admin.html Normal file
View file

@ -0,0 +1,28 @@
{% include "header.html" %}
<form id="auth-form" method="POST" action="/admin/" enctype="multipart/form-data">
<label for="username"> Administrator username</label>
<input id="username-field" placeholder="Username" type="username" autocomplete="off" name="username">
<label for="password"> Administrator password.</label>
<input id="password-field" placeholder="Password" type="password" autocomplete="off" name="password">
<button>Sign in</button>
{% if status == "incorrect" %}
<p>
Incorrect username or password.
</p>
{% endif %}
</form>
{% include "footer.html" %} {% if !args.pure_html %}
<style>
#auth-form {
background-color: var(--background-alt);
border-radius: 6px;
padding: 10px;
width: fit-content;
margin: auto;
margin-top: 2rem;
margin-bottom: 2rem;
}
</style>
{% endif %}

119
templates/auth_upload.html Normal file
View file

@ -0,0 +1,119 @@
{% include "header.html" %}
{% if encrypt_client %}
<form id="auth-form" method="POST" action="/{{path}}/{{id}}" enctype="multipart/form-data">
{% 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" required placeholder="Password" type="password" autocomplete="off">
<input id="password-hidden" name="password" type="hidden">
<button>Okay</button>
{% if status == "incorrect" %}
<b>
Incorrect password.
</b>
{% endif %}
</form>
<script>
const form = document.getElementById("auth-form");
const passwordField = document.getElementById("password-field");
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) {
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);
const res = aesjs.utils.utf8.fromBytes(decryptedBytes);
if (res.endsWith("!0K")) {
return res.substring(0, res.length - "!0K".length);
} else {
return null;
}
}
</script>
{% 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>
<input id="password-field" placeholder="Password" name="password" type="password" autocomplete="off" />
<button>Okay</button>
{% if status == "incorrect" %}
<b>
Incorrect password.
</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: var(--background-alt);
border-radius: 6px;
padding: 10px;
width: fit-content;
margin: auto;
margin-top: 2rem;
margin-bottom: 2rem;
}
</style>
{% endif %}

40
templates/edit.html Normal file
View file

@ -0,0 +1,40 @@
{% include "header.html" %}
<form action="/{{ path }}/{{ pasta.id_as_animals() }}" method="POST" enctype="multipart/form-data">
<h4>
Editing upload '{{ pasta.id_as_animals() }}'
</h4>
<label>Content</label>
<br>
<textarea style="width: 100%; min-height: 100px; font-family: monospace;" name="content" id="content" {% if status
!="incorrect" %} autofocus {% endif %}>{{ pasta.content_escaped() }}</textarea>
<br>
<div>
{% if pasta.readonly || pasta.encrypt_server %}
<div style="float: left; height: 90px;">
<label for="password">Re-enter Password <sup><a href="/guide#password"></a></sup></label><br>
<input {% if status=="incorrect" %} autofocus {% endif %} style="width: 130px; height: 28px;"
type="password" id="password" name="password" autocomplete="off" />
{% if status == "incorrect" %}
<p>
Incorrect password.
</p>
{% endif %}
</div>
{% endif %}
<div style="float: right; height: 90px; justify-content: end;">
<label for="password_field"></label><br>
<input style="width: 140px; float: right; background-color:
#2975D2; color: white;" id="submit-button" type="submit" value="Save" />
</td>
</div>
</div>
<br>
<br>
<br>
<br>
<br>
</form>
{% include "footer.html" %}

View file

@ -4,7 +4,7 @@
<b>Not Found</b>
<br>
<br>
<a href="/" > Go Home</a>
<a href="{{ args.public_path_as_str() }}/"> Go Home</a>
<br>
<br>
{% include "footer.html" %}
{% include "footer.html" %}

View file

@ -1,13 +1,15 @@
{% if !args.hide_footer %}
<hr>
<p style="font-size: smaller">
MicroBin by Daniel Szabo. Fork me on <a href="https://github.com/szabodanika/microbin">GitHub</a>!
Let's keep the Web <b>compact</b>, <b>accessible</b> and <b>humane</b>!
<p style="font-size: small; text-align: center;margin-top: 2rem;">
{% 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()|safe }} {%-
endif %}
</p>
{%- endif %}
</body>
</html>
</html>

82
templates/guide.html Normal file
View file

@ -0,0 +1,82 @@
{% include "header.html" %}
<a id="options">
<h2>Options</h2>
</a>
<a id="expiration">
<h3>Expiration</h3>
</a>
<p>
Use the expiration dropdown to choose how long you want your upload to exist.
When the selected time has expired, it will be removed from the server.
</p>
{% if args.enable_burn_after %}
<a id="burn-after">
<h3>Burn After</h3>
</a>
<p>
Use the burn after dropdown to set a limit on how many times your data can be
accessed before it will be removed from the server.
</p>
{%- endif %}
{% if args.highlightsyntax %}
<a id="syntax">
<h3>Syntax Highlighting</h3>
</a>
<p>
Use the syntax highlighting dropdown to enable syntax highlighting for your upload, making it easier to read.
You may choose to have the syntax highlighting done by your browser, which
will also recognise the language automatically. You can select server-side
highlighting, where you need to select the language yourself, but the code
will get highlighting without javascript.
</p>
{%- endif %}
<a id="password">
<h3>Password</h3>
</a>
<p>
Use the password field to set a password for your upload. This will encrypt
your data while stored on our server with your password, and you will need to
enter the password to access (in case of private and secret uploads) or to
modify (in case of read-only uploads). Your password is encrypted, and in case
of secret uploads, we never even see it.
</p>
<a id="privacy">
<h3>Privacy</h3>
</a>
<p>
Use this dropdown to select the level of protection your upload needs. Use
lower privacy levels if you or your organisation host MicroBin, and higher
privacy levels if you are using a public MicroBin service.
</p>
<h4>Level 1: Public</h4>
<p>This privacy level allows everyone to find, see, modify and remove your upload.</p>
<h4>Level 2: Unlisted (recommended)</h4>
<p>Unlisted uploads cannot be found unless someone knows its unique, random
identifier. If someone knows this identifier, they can see, modify and remove
the upload.</p>
<h4>Level 3: Read-only</h4>
<p>With this privacy setting, the upload cannot be found unless someone knows
its unique, random identifier. If someone knows this identifier, they can see
the contents but cannot modify or remove it without entering the password of
the upload.
</p>
<h4>Level 4: Private</h4>
<p>With this privacy setting, the upload cannot be found unless someone knows
its unique, random identifier. If someone knows this identifier, they cannot
see, modify or remove it without entering the password of the upload. Your
upload and its attachments are encrypted, so they are stored safely.</p>
<h4>Level 5: Secret</h4>
<p>With this privacy setting, the upload cannot be found unless someone knows
its unique, random identifier. If someone knows this identifier, they cannot
see, modify or remove it without entering the password of the upload. Your
browser sends us an already encrypted version, so the unencrypted data and
password never even leave your device. This option requires you to enter your
password many times when accessing your data, but is extremely safe.</p>
{% include "footer.html" %}

View file

@ -1,35 +1,62 @@
<!DOCTYPE html>
<html>
<head>
{% if args.title.as_ref().is_none() %}
<title>MicroBin</title>
{%- else %}
<title>{{ args.title.as_ref().unwrap() }}</title>
{%- endif %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if !args.pure_html %}
<link rel="stylesheet" href="/static/water.css">
{%- endif %}
</head>
<body style="
max-width: 720px;
margin: auto;
padding-left:0.5rem;
padding-right:0.5rem;
line-height: 1.5;
font-size: 1.1em;
">
<link rel="icon" type="image/svg+xml" href="{{ args.public_path_as_str() }}/static/favicon.ico">
<br>
<script type="text/javascript" src="{{ args.public_path_as_str() }}/static/aes.js"></script>
{% if !args.pure_html %} {% if args.custom_css.as_ref().is_none() ||
args.custom_css.as_ref().unwrap() == "" %}
<link rel="stylesheet" href="{{ args.public_path_as_str() }}/static/water.css">
{%- else %}
<link rel="stylesheet" href="{{ args.custom_css.as_ref().unwrap() }}">
{%- endif %} {%- endif %}
{% if !args.hide_header %}
</head>
{% if args.wide %}
<b style="margin-right: 0.5rem">
<i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> MicroBin
</b>
<body style="max-width: 1080px; margin: auto; padding-left:0.5rem;
padding-right:0.5rem; line-height: 1.5; font-size: 1.1em; padding-top: 2rem;">
{%- else %}
<a href="/" style="margin-right: 0.5rem; margin-left: 0.5rem">New Pasta</a>
<body style=" max-width: 800px; margin: auto; padding-left:0.5rem;
padding-right:0.5rem; padding-top: 2rem; line-height: 1.5; font-size: 1.1em; ">
{%- endif %}
<br>
{% if !args.hide_header %}
<a href="/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">Pasta List</a>
<div id="nav" style="margin-bottom: 1rem;">
<b style="margin-right: 0.5rem">
{% if !args.hide_logo %}
<!-- <i><span style="font-size:2.2rem;
margin-right:1rem">μ</span></i> -->
<a href="/"><img width=100 style="margin-bottom: -6px; margin-right:
0.5rem;" src="{{ args.public_path_as_str() }}/static/logo.png"></a>
{%- endif %} {% if args.title.as_ref().is_none() %} {%- else %} {{
args.title.as_ref().unwrap() }} {%- endif %}
</b>
<a href="https://github.com/szabodanika/microbin" style="margin-right: 0.5rem; margin-left: 0.5rem">GitHub</a>
<hr>
<a href="{{ args.public_path_as_str() }}/" style="margin-right: 0.5rem;
margin-left: 0.5rem">New</a>
{%- endif %}
{% if !args.no_listing %}
<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;
margin-left: 0.5rem">Guide</a>
</div>
<!-- <hr> -->
{%- endif %}

View file

@ -1,27 +1,448 @@
{% include "header.html" %}
<form action="upload" method="POST" enctype="multipart/form-data">
<br>
<label for="expiration">Expiration</label><br>
<select style="width: 100%;" name="expiration" id="expiration">
<optgroup label="Expire">
<option value="1min">1 minute</option>
<option value="10min">10 minutes</option>
<option value="1hour">1 hour</option>
<option selected value="24hour">24 hours</option>
<option value="1week">1 week</option>
</optgroup>
<option value="never">Never Expire</option>
</select>
<form id="pasta-form" action="upload" method="POST" enctype="multipart/form-data">
<br>
<div id="settings">
<div>
<label for="expiration">Expiration <sup> <a href="/guide#expiration"></a></sup></label><br>
<select style="width: 100%;" name="expiration" id="expiration">
<optgroup label="Expire after">
{% if args.default_expiry == "1min" %}
<option selected value="1min">
{%- else %}
<option value="1min">
{%- endif %} 1 minute
</option>
{% if args.default_expiry == "10min" %}
<option selected value="10min">
{%- else %}
<option value="10min">
{%- endif %} 10 minutes
</option>
{% if args.default_expiry == "1hour" %}
<option selected value="1hour">
{%- else %}
<option value="1hour">
{%- endif %} 1 hour
</option>
{% if args.default_expiry == "24hour" %}
<option selected value="24hour">
{%- else %}
<option value="24hour">
{%- endif %} 24 hours
</option>
{% if args.default_expiry == "3days" %}
<option selected value="3days">
{%- else %}
<option value="3days">
{%- endif %} 3 days
</option>
{% if args.default_expiry == "1week" %}
<option selected value="1week">
{%- else %}
<option value="1week">
{%- endif %} 1 week
</option>
</optgroup>
{% if !args.eternal_pasta %} {% if args.default_expiry ==
"never" %}
<option selected value="never">
{%- else %}
<option value="never">{%- endif %} Never Expire
</option>
{%- endif %}
</select>
</div>
{% if args.enable_burn_after %}
<div>
<label for="burn_after">Burn After <sup> <a href="/guide#burn-after"></a></sup></label><br>
<select style="width: 100%;" name="burn_after" id="burn_after">
{% if args.default_burn_after == 0 %}
<option selected value="0">
{%- else %}
<option value="0">
{%- endif %} No Limit
</option>
<optgroup label="Burn after">
{% if args.default_burn_after == 1 %}
<option selected value="1">
{%- else %}
<option value="1">
{%- endif %} First Read
</option>
{% if args.default_burn_after == 10 %}
<option selected value="10">
{%- else %}
<option value="10">
{%- endif %} 10th Read
</option>
{% if args.default_burn_after == 100 %}
<option selected value="100">
{%- else %}
<option value="100">
{%- endif %} 100th Read
</option>
{% if args.default_burn_after == 1000 %}
<option selected value="1000">
{%- else %}
<option value="1000">
{%- endif %} 1000th Read
</option>
{% if args.default_burn_after == 10000 %}
<option selected value="10000">
{%- else %}
<option value="10000">
{%- endif %} 10000th Read
</option>
</optgroup>
</select>
</div>
{%- endif %}
{% 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">
<option value="none">None</option>
<optgroup label="Client-Rendered">
<option value="auto">Automatic</option>
</optgroup>
<optgroup label="Server-Rendered">
<option value="sh">Bash Shell</option>
<option value="c">C</option>
<option value="cpp">C++</option>
<option value="cs">C#</option>
<option value="pas">Delphi</option>
<option value="erl">Erlang</option>
<option value="go">Go</option>
<option value="hs">Haskell</option>
<option value="html">HTML</option>
<option value="lua">Lua</option>
<option value="lisp">Lisp</option>
<option value="java">Java</option>
<option value="js">JavaScript</option>
<option value="kt">Kotlin</option>
<option value="py">Python</option>
<option value="php">PHP</option>
<option value="r">R</option>
<option value="rs">Rust</option>
<option value="rb">Ruby</option>
<option value="sc">Scala</option>
<option value="swift">Swift</option>
<!-- no toml support ;-( -->
<option value="json">TOML</option>
<option value="yaml">YAML</option>
<option value="json">JSON</option>
<option value="xml">XML</option>
</optgroup>
</optgroup>
</select>
</div>
{%- else %}
<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="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 || 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" />
</div>
{%- endif %}
</div>
<label>Content</label>
<br>
<textarea style="width: 100%; min-height: 100px" name="content" autofocus></textarea>
<br>
<label>File attachment</label>
<br>
<input style="width: 100%;" type="file" id="file" name="file">
<br>
<input style="width: 120px; background-color: limegreen" ; type="submit" value="Save"/>
<br>
<textarea style="width: 100%; min-height: 100px; margin-bottom: 2em; font-family: monospace;" id="content-input"
autofocus placeholder="Type something here."></textarea>
<div>
{% if !args.no_file_upload %}
<div id="file-select">
<label for="file" id="attach-file-button-label"><a role="button" id="attach-file-button">Select or drop file
attachment</a></label>
<br>
<input type="file" id="file" name="file" />
</div>
{% 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>
</div>
<input type="hidden" name="content" id="content">
<input type="hidden" name="encrypt_client" id="encrypt_client">
{% 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>
<br>
<br>
<script>
const form = document.getElementById("pasta-form");
const submitButton = document.getElementById("submit-button");
const passwordField = document.getElementById("password_field");
const privacyDropdown = document.getElementById("privacy");
const contentInput = document.getElementById("content-input");
const content = document.getElementById("content");
const attachFileButton = document.getElementById('attach-file-button');
const dropContainer = document.getElementById('pasta-form');
const hiddenFileButton = document.getElementById('file');
const hiddenRandomKeyField = document.getElementById("random_key");
const hiddenEncryptedRandomKeyField = document.getElementById("encrypted_random_key");
const hiddenPlainKeyField = document.getElementById("plain_key");
const hiddenEncryptedClientSide = document.getElementById("encrypt_client");
const te = new TextEncoder();
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('');
hiddenRandomKeyField.value = randomKey;
hiddenEncryptedRandomKeyField.value = encryptWithPassword(passwordField.value, randomKey);
if (contentInput.value.trim() != "") {
content.value = encryptWithPassword(passwordField.value.trim(), contentInput.value);
} else {
content.value = contentInput.value;
}
hiddenPlainKeyField.name = "";
// {% if !args.no_file_upload %}
await encryptFile();
// {%- endif %}
} else {
hiddenPlainKeyField.value = passwordField.value;
hiddenEncryptedClientSide.name = "";
hiddenEncryptedRandomKeyField.name = "";
hiddenRandomKeyField.name = "";
content.value = contentInput.value;
}
} else {
if (privacyDropdown.value != "public" && privacyDropdown.value != "unlisted") {
passwordField.focus();
return false;
}
hiddenEncryptedClientSide.name = "";
content.value = contentInput.value;
}
// {%- else %}
hiddenEncryptedClientSide.name = "";
content.value = contentInput.value;
// {%- endif %}
if (contentInput.value.trim() == "" && (hiddenFileButton == undefined || hiddenFileButton.files.length == 0)) {
contentInput.focus();
return false;
}
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) {
const passwordBytes = aesjs.utils.utf8.toBytes(password.padStart(32, "#"));
const plaintextBytes = aesjs.utils.utf8.toBytes(plaintext + "!0K");
const aesCtr = new aesjs.ModeOfOperation.ctr(passwordBytes);
const encryptedBytes = aesCtr.encrypt(plaintextBytes);
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;
const fileSizeMb = fileSize / 1024 ** 2;
if (privacyDropdown.value == "secret") {
if (fileSizeMb >
parseInt("{{ args.max_file_size_encrypted_mb }}")) {
attachFileButton.textContent = "Please select a file smaller than {{ args.max_file_size_encrypted_mb }} MB";
this.value = "";
return true;
}
} else {
if (fileSizeMb >
parseInt("{{ args.max_file_size_unencrypted_mb }}")) {
attachFileButton.textContent = "Please select a file smaller than {{ args.max_file_size_unencrypted_mb }} MB";
this.value = "";
return true;
}
}
}
return false;
}
function encryptFile() {
return new Promise((resolve, reject) => {
if (hiddenFileButton.files.length > 0) {
const file = hiddenFileButton.files[0];
const reader = new FileReader();
reader.onload = function (event) {
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 });
let container = new DataTransfer();
container.items.add(encryptedFile);
hiddenFileButton.files = container.files;
resolve(encryptedFile);
};
reader.readAsArrayBuffer(file);
} else {
resolve();
}
});
}
hiddenFileButton.addEventListener('change', function () {
attachFileButton.textContent = "Attached: " + this.files[0].name;
fileOversized();
});
dropContainer.ondragover = dropContainer.ondragenter = function (evt) {
evt.preventDefault();
if (hiddenFileButton.files.length == 0) {
attachFileButton.textContent = "Drop your file here";
} else {
attachFileButton.textContent = "Drop your file here to replace " + hiddenFileButton.files[0].name;
}
};
dropContainer.ondrop = function (evt) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(evt.dataTransfer.files[0]);
hiddenFileButton.files = dataTransfer.files;
attachFileButton.textContent = "Attached: " + hiddenFileButton.files[0].name;
evt.preventDefault();
};
// {%- endif %}
</script>
<style>
input::file-selector-button {
display: none;
}
#settings {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(auto-fit, 152px);
grid-template-rows: repeat(1, 90px);
margin-bottom: 1rem;
}
select {
height: 3rem;
}
/* {% if !args.pure_html %} */
#attach-file-button-label {
cursor: pointer;
padding: 0.5rem;
border: #2975D2 2px dotted;
border-radius: 6px;
padding-left: 1rem;
padding-right: 1rem;
font-size: smaller;
min-width: 235px;
text-align: center;
}
/* {% endif %} */
#file {
display: none;
}
#file-select {
float: left;
}
</style>
{% 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,9 +0,0 @@
{% include "header.html" %}
<a style="margin-right: 0.5rem" href="/raw/{{pasta.id_as_animals()}}">Raw Text Content</a>
{% if pasta.file != "no-file" %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">Attached file '{{pasta.file}}'</a>
{%- endif %}
<a style="margin-right: 0.5rem; margin-left: 0.5rem" href="/remove/{{pasta.id_as_animals()}}">Remove</a>
<pre><code>{{pasta}}</code></pre>
{% include "footer.html" %}

View file

@ -1,101 +0,0 @@
{% include "header.html" %}
{% if pastas.is_empty() %}
<br>
<p>
No pastas yet. 😔 Create one <a href="/">here</a>.
</p>
<br>
{%- else %}
<br>
<table style="width: 100%">
<thead>
<tr>
<th colspan="4">Pastas</th>
</tr>
<tr>
<th>
Key
</th>
<th>
Created
</th>
<th>
Expiration
</th>
<th>
</th>
</tr>
</thead>
<tbody>
{% for pasta in pastas %}
{% if pasta.pasta_type == "text" %}
<tr>
<td>
<a href="/pasta/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
{% if pasta.file != "no-file" %}
<a style="margin-right:1rem" href="/file/{{pasta.id_as_animals()}}/{{pasta.file}}">File</a>
{%- endif %}
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<table>
<thead>
<tr>
<th colspan="4">URL Redirects</th>
</tr>
<tr >
<th>
Key
</th>
<th>
Created
</th>
<th>
Expiration
</th>
<th>
</th>
</tr>
</thead>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" %}
<tr>
<td>
<a href="/url/{{pasta.id_as_animals()}}">{{pasta.id_as_animals()}}</a>
</td>
<td>
{{pasta.created_as_string()}}
</td>
<td>
{{pasta.expiration_as_string()}}
</td>
<td>
<a style="margin-right:1rem" href="/raw/{{pasta.id_as_animals()}}">Raw</a>
<a href="/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
{%- endif %}
{% include "footer.html" %}

29
templates/qr.html Normal file
View file

@ -0,0 +1,29 @@
{% include "header.html" %}
<div style="float: left">
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">Back to Upload</a>
</div>
<div style="text-align: center; padding: 3rem;">
{% if pasta.pasta_type == "url" %}
<a href="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% else %}
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% endif %}
</div>
<style>
.copy-text-button,
.copy-url-button {
font-size: small;
padding: 4px;
width: 6rem;
}
</style>
{% include "footer.html" %}

394
templates/upload.html Normal file
View file

@ -0,0 +1,394 @@
{% include "header.html" %}
<div style="float: left">
{% if pasta.content != "" %}
<button id="copy-text-button" class="small-button" style="margin-right:
0.5rem">
Copy Text
</button>
{% if args.public_path_as_str() != "" && pasta.pasta_type == "url" %}
<button id="copy-redirect-button" class="small-button" style="margin-right:
0.5rem">
Copy Redirect
</button>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path_as_str() }}/raw/{{pasta.id_as_animals()}}">Raw Text
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 && !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() }}/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
</button>
{%- endif %}
</div>
<br>
<br>
{% 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">
<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>
Download {{pasta.file.as_ref().unwrap().name()}}
[{{pasta.file.as_ref().unwrap().size}}]
</b>
</button>
{%- endif %}
</div>
</span>
{%- endif %}
<br>
{% if pasta.content != "" %}
<div class="code-container">
<div style="clear: both;">
{% if pasta.extension == "auto" || pasta.encrypt_client %}
<pre><code id="code">{{pasta.content_escaped()}}</code></pre>
{% else if args.highlightsyntax %}
<pre><code id="code">{{pasta.content_syntax_highlighted()}}</code></pre>
{% else %}
<pre><code id="code">{{pasta.content_not_highlighted()}}</code></pre>
{%- endif %}
</div>
</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 %}
<img id="embed" src="{{ args.public_path_as_str()}}/file/{{pasta.id_as_animals()}}" height="300" />
<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" download>
<button class="download-button" autofocus>
Download
</button>
</a>
</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>
<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()}}" download id="download-link">
<button class="download-button">
Download
</button>
</a>
</span>
{%- endif %}
<div>
{% if args.show_read_stats %} {% if pasta.read_count == 1 %}
<p style="font-size: small">Read {{pasta.read_count}} time, last
{{pasta.last_read_time_ago_as_string()}}</p>
{%- else %}
<p style="font-size: small">Read {{pasta.read_count}} times, last
{{pasta.last_read_time_ago_as_string()}}</p>
{%- endif %} {%- endif %}
</div>
<br>
<script type="text/javascript" src="{{ args.public_path_as_str() }}/static/highlight/highlight.min.js"></script>
<link rel="stylesheet" href="{{ args.public_path_as_str()}}/static/highlight/highlight.min.css">
<script>
const copyURLBtn = document.getElementById("copy-url-button")
const copyTextBtn = document.getElementById("copy-text-button")
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() }}/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();
// {% if pasta.extension == "auto" && !pasta.encrypt_client %}
onload = (event) => {
contentElement.innerHTML = content;
hljs.highlightAll();
contentElement.innerHTML =
wrapStringInCodeLines(contentElement.innerHTML);
};
// {% endif %}
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function wrapStringInCodeLines(str) {
const lines = str.split(/\r?\n/); // split the string into an array of lines
const wrappedLines = lines.map((line) => `<code-line>${line}</code-line>`); // wrap each line in a "code-line" tag
return wrappedLines.join("\n"); // join the wrapped lines back into a single string with line breaks
}
const decodeEntity = (inputStr) => {
var textarea = document.createElement("textarea");
textarea.innerHTML = inputStr;
return textarea.value;
}
if (copyURLBtn) {
copyURLBtn.addEventListener("click", () => {
navigator.clipboard.writeText(url)
copyURLBtn.innerHTML = "Copied"
setTimeout(() => {
copyURLBtn.innerHTML = "Copy URL"
}, 1000)
})
}
// it will be undefined when the element does not exist on non-url pastas
if (copyRedirectBtn) {
copyRedirectBtn.addEventListener("click", () => {
navigator.clipboard.writeText(redirect_url)
copyRedirectBtn.innerHTML = "Copied"
setTimeout(() => {
copyRedirectBtn.innerHTML = "Copy Redirect"
}, 1000)
})
}
if (copyTextBtn) {
copyTextBtn.addEventListener("click", () => {
const decodeContent = decodeEntity(content)
navigator.clipboard.writeText(decodeContent)
copyTextBtn.innerHTML = "Copied"
setTimeout(() => {
copyTextBtn.innerHTML = "Copy Text"
}, 1000)
})
}
// {% if pasta.encrypt_client %}
const decryptDiv = document.getElementById('decryption');
const decryptButton = document.getElementById('decrypt-button');
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
if (passwordField.value.trim() == "") {
passwordField.focus();
return false;
}
// Fetch encrypted file from server
const formData = new FormData();
// {% if pasta.encrypted_key.is_some() %}
let key = decryptWithPassword(passwordField.value.trim(), "{{ pasta.encrypted_key.as_ref().unwrap() }}");
// {%- endif %}
formData.append('password', key);
const response = await fetch('/secure_file/{{ pasta.id_as_animals() }}', {
method: 'POST',
body: formData,
})
// {% if pasta.file.is_some() %}
const encryptedFile = await response.text();
// Decrypt file contents
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:application/octet-stream;${encodeURIComponent(decryptedContents)}`;
// Create temporary anchor element
const tempAnchorEl = document.createElement('a');
// 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();
// {%- endif %}
});
// {% endif %}
decryptButton.addEventListener("click", () => {
password = passwordField.value;
content = contentDecrypted = escapeHtml(decryptWithPassword(password, content));
if (contentDecrypted) {
contentElement.innerHTML = contentDecrypted;
// {% if pasta.extension == "auto" %}
hljs.highlightAll();
// {% endif %}
contentElement.innerHTML = wrapStringInCodeLines(contentElement.innerHTML);
// decryptDiv.style.display = 'none';
}
});
function decryptWithPassword(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);
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>
<style>
code-line {
counter-increment: line;
text-align: right;
float: left;
clear: left;
}
code-line::before {
content: counter(line);
display: inline-block;
padding-left: auto;
margin-left: auto;
text-align: left;
width: 1.8rem;
border-right: 1px solid lightgrey;
color: grey;
margin-right: 0.4rem;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#code {
min-height: 2rem;
}
#embed {
background-color: #f7f7f7;
border-radius: 6px;
margin-top: 1rem;
margin-bottom: 1rem;
width: fit-content;
margin-left: auto;
margin-right: auto;
max-height: 480px;
display: flex;
justify-content: center;
align-items: center;
}
.download-button {
margin-left: 1rem;
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
cursor: pointer;
}
.code-container {
position: relative;
}
.hidden {
display: none;
}
.small-button {
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
cursor: pointer;
}
</style>
{% if !args.pure_html %}
<style>
#decryption {
background-color: #f7f7f7;
border-radius: 6px;
padding: 10px;
width: fit-content;
}
</style>
{% endif %}
{% include "footer.html" %}