Compare commits

...

137 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
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
70 changed files with 11885 additions and 1675 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

2
.github/FUNDING.yml vendored
View file

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

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

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

View file

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

BIN
.github/index.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
.github/logo.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -1,45 +0,0 @@
name: Docker Image
on:
push:
tags:
- v[0-9]+.[0-9]+.[0-9]+*
jobs:
docker_image:
name: Build & push docker image to DockerHub
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- 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: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & push
uses: docker/build-push-action@v3
with:
platforms: linux/amd64, linux/arm64
push: ${{ github.ref_type == 'tag' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View file

@ -1,4 +1,4 @@
name: GitHub Release
name: GitHub and Docker Release
on:
push:
@ -7,9 +7,10 @@ on:
jobs:
release:
name: Publish to Github Releases
name: Publish to Github Relases
outputs:
rc: ${{ steps.check-tag.outputs.rc }}
strategy:
matrix:
include:
@ -92,6 +93,10 @@ jobs:
target: ${{ matrix.target }}
toolchain: stable
profile: minimal # minimal component installation (ie, no documentation)
- name: Install OpenSSL
if: runner.os == 'Linux'
run: sudo apt-get install -y libssl-dev
- name: Show Version Information (Rust, cargo, GCC)
shell: bash
@ -144,11 +149,48 @@ jobs:
fi
- name: Publish Archive
uses: softprops/action-gh-release@v0.1.5
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 }}
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 }}

5
.gitignore vendored
View file

@ -9,4 +9,7 @@ target/
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
pasta_data/*
pasta_data/*
microbin_data/*
*.env
**/**/microbin-data

2702
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -2,12 +2,14 @@ FROM rust:latest as build
WORKDIR /app
COPY . .
RUN \
DEBIAN_FRONTEND=noninteractive \
apt-get update &&\
apt-get -y install ca-certificates tzdata &&\
apt-get -y install ca-certificates tzdata
COPY . .
RUN \
CARGO_NET_GIT_FETCH_WITH_CLI=true \
cargo build --release
@ -17,16 +19,18 @@ 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/zoneinfo
/usr/share/
COPY --from=build \
/etc/ssl/certs/ca-certificates.crt \
/etc/ssl/certs/ca-certificates.crt
# copy built exacutable
# copy built executable
COPY --from=build \
/app/target/release/microbin \
/usr/bin/microbin
@ -34,4 +38,4 @@ COPY --from=build \
# Expose webport used for the webserver to the docker runtime
EXPOSE 8080
ENTRYPOINT ["microbin"]
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,70 +0,0 @@
![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)
[![Docker Image](https://github.com/szabodanika/microbin/actions/workflows/docker.yml/badge.svg)](https://hub.docker.com/r/danielszabo99/microbin)
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?
Install from Cargo:
`cargo install microbin`
And run with your custom configuration:
`microbin --port 8080 --public-path https://myserver.net --highlightsyntax --editable`
Or get the Docker image from [Dockerhub: danielszabo99/microbin](https://hub.docker.com/r/danielszabo99/microbin)
On our website [microbin.eu](microbin.eu) you will find the following:
- [Screenshots](https://microbin.eu/screenshots/)
- [Quickstart Guide](https://microbin.eu/quickstart/)
- [Documentation](https://microbin.eu/documentation/)
- [Donations and Sponsorhip](https://microbin.eu/donate/)
- [Community](https://microbin.eu/community/)
### Features
- Is very small
- Entirely self-contained executable, MicroBin is a single file!
- Animal names instead of random numbers for pasta identifiers (64 animals)
- File uploads (eg. server.com/file/pig-dog-cat)
- Raw text serving (eg. server.com/raw/pig-dog-cat)
- URL shortening and redirection
- QR code support
- Very simple database (JSON + files) for portability, easy backups and integration
- Listing and manually removing pastas (/pastalist)
- Private and public, editable and final, automatically and never expiring pastas
- Syntax highlighting
- Automatic dark mode and custom styling support with very little CSS and only vanilla JS (see [water.css](https://github.com/kognise/water.css))
- Most of the above can be toggled on and off!
### What is a "pasta" anyway?
In microbin, a pasta can be:
- A text that you want to paste from one machine to another, eg. some code,
- A file that you want to share, eg. a video that is too large for Discord, a zip with a code project in it or an image,
- A URL redirect.
### When is MicroBin useful?
You can use MicroBin
- As a URL shortener/redirect service,
- To send long texts to other people,
- To send large files to other people,
- To serve content on the web, eg. configuration files for testing, images, or any other file content using the Raw functionality,
- To move files between your desktop and a server you access from the console,
- As a "postbox" service where people can upload their files or texts, but they cannot see or remove what others sent you - just disable the pastalist page
- To take notes! Simply create an editable pasta.
...and many other things, why not get creative?
### License
MicroBin and MicroBin.eu are available under the BSD 3-Clause License.
© Dániel Szabó 2022

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

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

View file

@ -1,5 +1,6 @@
use clap::Parser;
use lazy_static::lazy_static;
use serde::Serialize;
use std::convert::Infallible;
use std::fmt;
use std::net::IpAddr;
@ -9,14 +10,20 @@ lazy_static! {
pub static ref ARGS: Args = Args::parse();
}
#[derive(Parser, Debug, Clone)]
#[derive(Parser, Debug, Clone, Serialize)]
#[clap(author, version, about, long_about = None)]
pub struct Args {
#[clap(long, env = "MICROBIN_AUTH_USERNAME")]
pub auth_username: Option<String>,
#[clap(long, env = "MICROBIN_BASIC_AUTH_USERNAME")]
pub auth_basic_username: Option<String>,
#[clap(long, env = "MICROBIN_AUTH_PASSWORD")]
pub auth_password: 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,
@ -51,12 +58,24 @@ pub struct Args {
#[clap(long, env = "MICROBIN_PURE_HTML")]
pub pure_html: bool,
#[clap(long, env="MICROBIN_PUBLIC_PATH", default_value_t = PublicUrl(String::from("")))]
pub public_path: PublicUrl,
#[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>,
@ -78,12 +97,18 @@ pub struct Args {
#[clap(long, env = "MICROBIN_QR")]
pub qr: bool,
#[clap(long, env = "MICROBIN_NO_ETERNAL_PASTA")]
pub no_eternal_pasta: 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,
@ -92,9 +117,105 @@ pub struct Args {
#[clap(long, env = "MICROBIN_HASH_IDS")]
pub hash_ids: bool,
#[clap(long, env = "MICROBIN_LIST_SERVER")]
pub list_server: bool,
#[clap(long, env = "MICROBIN_DISABLE_TELEMETRY")]
pub disable_telemetry: bool,
#[clap(long, env = "MICROBIN_DISABLE_UPDATE_CHECKING")]
pub disable_update_checking: bool,
#[clap(long, env = "MICROBIN_ENCRYPTION_CLIENT_SIDE")]
pub encryption_client_side: bool,
#[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,
}
#[derive(Debug, Clone)]
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 {

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

View file

@ -1,10 +1,11 @@
use crate::dbio::save_to_file;
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::is_valid_url;
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;
@ -18,25 +19,65 @@ use std::time::{SystemTime, UNIX_EPOCH};
#[template(path = "index.html")]
struct IndexTemplate<'a> {
args: &'a ARGS,
status: String,
}
#[get("/")]
pub async fn index() -> impl Responder {
HttpResponse::Ok()
.content_type("text/html")
.body(IndexTemplate { args: &ARGS }.render().unwrap())
HttpResponse::Ok().content_type("text/html").body(
IndexTemplate {
args: &ARGS,
status: String::from(""),
}
.render()
.unwrap(),
)
}
#[get("/{status}")]
pub async fn index_with_status(param: web::Path<String>) -> HttpResponse {
let status = param.into_inner();
return HttpResponse::Ok().content_type("text/html").body(
IndexTemplate {
args: &ARGS,
status,
}
.render()
.unwrap(),
);
}
pub fn expiration_to_timestamp(expiration: &str, timenow: i64) -> i64 {
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> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish());
}
let mut pastas = data.pastas.lock().unwrap();
let timenow: i64 = match SystemTime::now().duration_since(UNIX_EPOCH) {
@ -49,51 +90,84 @@ pub async fn create(
let mut new_pasta = Pasta {
id: rand::thread_rng().gen::<u16>() as u64,
content: String::from("No Text Content"),
content: String::from(""),
file: None,
extension: String::from(""),
private: false,
editable: 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: 0,
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? {
match field.name() {
"editable" => {
// while let Some(_chunk) = field.try_next().await? {}
new_pasta.editable = true;
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;
}
"private" => {
// while let Some(_chunk) = field.try_next().await? {}
new_pasta.private = true;
"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 = 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" => {
if ARGS.no_eternal_pasta {
timenow + 60 * 60 * 24 * 7
} else {
0
}
}
_ => {
log::error!("{}", "Unexpected expiration time!");
timenow + 60 * 60 * 24 * 7
}
};
new_pasta.expiration =
expiration_to_timestamp(std::str::from_utf8(&chunk).unwrap(), timenow);
}
continue;
@ -101,7 +175,8 @@ pub async fn create(
"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
// give an extra read because the user will be
// redirected to the pasta page automatically
"1" => 2,
"10" => 10,
"100" => 100,
@ -122,7 +197,7 @@ pub async fn create(
while let Some(chunk) = field.try_next().await? {
content.push_str(std::str::from_utf8(&chunk).unwrap().to_string().as_str());
}
if content.len() > 0 {
if !content.is_empty() {
new_pasta.content = content;
new_pasta.pasta_type = if is_valid_url(new_pasta.content.as_str()) {
@ -133,7 +208,7 @@ pub async fn create(
}
continue;
}
"syntax-highlight" => {
"syntax_highlight" => {
while let Some(chunk) = field.try_next().await? {
new_pasta.extension = std::str::from_utf8(&chunk).unwrap().to_string();
}
@ -144,7 +219,7 @@ pub async fn create(
continue;
}
let path = field.content_disposition().get_filename();
let path = field.content_disposition().and_then(|cd| cd.get_filename());
let path = match path {
Some("") => continue,
@ -152,7 +227,7 @@ pub async fn create(
None => continue,
};
let mut file = match PastaFile::from_unsanitized(&path) {
let mut file = match PastaFile::from_unsanitized(path) {
Ok(f) => f,
Err(e) => {
warn!("Unsafe file name: {e:?}");
@ -161,13 +236,15 @@ pub async fn create(
};
std::fs::create_dir_all(format!(
"./pasta_data/public/{}",
"{}/attachments/{}",
ARGS.data_dir,
&new_pasta.id_as_animals()
))
.unwrap();
let filepath = format!(
"./pasta_data/public/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
&new_pasta.id_as_animals(),
&file.name()
);
@ -176,6 +253,12 @@ pub async fn create(
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??;
}
@ -190,18 +273,68 @@ pub async fn create(
}
}
if ARGS.readonly && ARGS.uploader_password.is_some() {
if uploader_password != ARGS.uploader_password.as_ref().unwrap().to_owned() {
return Ok(HttpResponse::Found()
.append_header(("Location", "/incorrect"))
.finish());
}
}
let id = new_pasta.id;
if plain_key != *"" && new_pasta.readonly {
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);
save_to_file(&pastas);
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)
};
Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/pasta/{}", ARGS.public_path, slug)))
.finish())
if encrypt_server {
Ok(HttpResponse::Found()
.append_header(("Location", format!("/auth/{}/success", slug)))
.finish())
} else {
Ok(HttpResponse::Found()
.append_header((
"Location",
format!("{}/upload/{}", ARGS.public_path_as_str(), slug),
))
.finish())
}
}

View file

@ -1,9 +1,9 @@
use crate::args::Args;
use crate::dbio::save_to_file;
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::remove_expired;
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};
@ -15,6 +15,8 @@ use futures::TryStreamExt;
struct EditTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
path: &'a String,
status: &'a String,
}
#[get("/edit/{id}")]
@ -22,9 +24,9 @@ pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
let mut pastas = data.pastas.lock().unwrap();
let id = if ARGS.hash_ids {
hashid_to_u64(&*id).unwrap_or(0)
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&*id.into_inner()).unwrap_or(0)
to_u64(&id.into_inner()).unwrap_or(0)
};
remove_expired(&mut pastas);
@ -33,13 +35,25 @@ pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
if pasta.id == id {
if !pasta.editable {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.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: &pasta,
pasta,
args: &ARGS,
path: &String::from("edit"),
status: &String::from(""),
}
.render()
.unwrap(),
@ -52,22 +66,248 @@ pub async fn get_edit(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
.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> {
if ARGS.readonly {
return Ok(HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.finish());
}
let id = if ARGS.hash_ids {
hashid_to_u64(&*id).unwrap_or(0)
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&*id.into_inner()).unwrap_or(0)
to_u64(&id.into_inner()).unwrap_or(0)
};
let mut pastas = data.pastas.lock().unwrap();
@ -75,28 +315,61 @@ pub async fn post_edit(
remove_expired(&mut pastas);
let mut new_content = String::from("");
let mut password = String::from("");
while let Some(mut field) = payload.try_next().await? {
match field.name() {
"content" => {
while let Some(chunk) = field.try_next().await? {
new_content = std::str::from_utf8(&chunk).unwrap().to_string();
}
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 {
pastas[i].content.replace_range(.., &*new_content);
save_to_file(&pastas);
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!("{}/pasta/{}", ARGS.public_path, pastas[i].id_as_animals()),
format!(
"{}/upload/{}",
ARGS.public_path_as_str(),
pastas[i].id_as_animals()
),
))
.finish());
} else {

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

View file

@ -1,42 +0,0 @@
use crate::args::{Args, ARGS};
use crate::pasta::Pasta;
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
#[derive(Template)]
#[template(path = "info.html")]
struct Info<'a> {
args: &'a Args,
pastas: &'a Vec<Pasta>,
status: &'a String,
version_string: &'a String,
message: &'a String,
}
#[get("/info")]
pub async fn info(data: web::Data<AppState>) -> HttpResponse {
// get access to the pasta collection
let mut pastas = data.pastas.lock().unwrap();
// todo status report more sophisticated
let mut status = "OK";
let mut message = "";
if ARGS.public_path.to_string() == "" {
status = "WARNING";
message = "Warning: No public URL set with --public-path parameter. QR code and URL Copying functions have been disabled"
}
HttpResponse::Ok().content_type("text/html").body(
Info {
args: &ARGS,
pastas: &pastas,
status: &String::from(status),
version_string: &String::from("1.2.0-20221107"),
message: &String::from(message),
}
.render()
.unwrap(),
)
}

View file

@ -7,17 +7,17 @@ use crate::util::misc::remove_expired;
use crate::AppState;
#[derive(Template)]
#[template(path = "pastalist.html")]
struct PastaListTemplate<'a> {
#[template(path = "list.html")]
struct ListTemplate<'a> {
pastas: &'a Vec<Pasta>,
args: &'a Args,
}
#[get("/pastalist")]
#[get("/list")]
pub async fn list(data: web::Data<AppState>) -> HttpResponse {
if ARGS.no_listing {
return HttpResponse::Found()
.append_header(("Location", format!("{}/", ARGS.public_path)))
.append_header(("Location", format!("{}/", ARGS.public_path_as_str())))
.finish();
}
@ -25,8 +25,11 @@ pub async fn list(data: web::Data<AppState>) -> HttpResponse {
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(
PastaListTemplate {
ListTemplate {
pastas: &pastas,
args: &ARGS,
}

View file

@ -1,32 +1,37 @@
use crate::args::{Args, ARGS};
use crate::dbio::save_to_file;
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_web::rt::time;
use actix_web::{get, web, HttpResponse};
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 = "pasta.html", escape = "none")]
#[template(path = "upload.html", escape = "none")]
struct PastaTemplate<'a> {
pasta: &'a Pasta,
args: &'a Args,
}
#[get("/pasta/{id}")]
pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpResponse {
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)
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&*id.into_inner()).unwrap_or(0)
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
@ -44,11 +49,39 @@ pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
}
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 = pastas[index].read_count + 1;
pastas[index].read_count += 1;
// save the updated read count
save_to_file(&pastas);
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(
@ -60,6 +93,10 @@ pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
.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(),
@ -72,25 +109,56 @@ pub async fn getpasta(data: web::Data<AppState>, id: web::Path<String>) -> HttpR
// 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
// 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 {
#[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)
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&*id.into_inner()).unwrap_or(0)
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
@ -110,10 +178,10 @@ pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> Ht
if found {
// increment read count
pastas[index].read_count = pastas[index].read_count + 1;
pastas[index].read_count += 1;
// save the updated read count
save_to_file(&pastas);
update(Some(&pastas), Some(&pastas[index]));
// send redirect if it's a url pasta
if pastas[index].pasta_type == "url" {
@ -133,6 +201,9 @@ pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> Ht
// 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 {
@ -142,22 +213,34 @@ pub async fn redirecturl(data: web::Data<AppState>, id: web::Path<String>) -> Ht
}
}
// otherwise
// send pasta not found error
// 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>) -> String {
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)
hashid_to_u64(&id).unwrap_or(0)
} else {
to_u64(&*id.into_inner()).unwrap_or(0)
to_u64(&id.into_inner()).unwrap_or(0)
};
// remove expired pastas (including this one if needed)
@ -175,8 +258,112 @@ pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> St
}
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 = pastas[index].read_count + 1;
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) {
@ -191,13 +378,28 @@ pub async fn getrawpasta(data: web::Data<AppState>, id: web::Path<String>) -> St
pastas[index].last_read = timenow;
// save the updated read count
save_to_file(&pastas);
update(Some(&pastas), Some(&pastas[index]));
// send raw content of pasta
return pastas[index].content.to_owned();
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
String::from("Pasta not found! :-(")
// 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)
}

View file

@ -7,8 +7,6 @@ use crate::util::misc::{self, remove_expired};
use crate::AppState;
use actix_web::{get, web, HttpResponse};
use askama::Template;
use qrcode_generator::QrCodeEcc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Template)]
#[template(path = "qr.html", escape = "none")]
@ -44,10 +42,14 @@ pub async fn getqr(data: web::Data<AppState>, id: web::Path<String>) -> HttpResp
}
if found {
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /pasta endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
// generate the QR code as an SVG - if its a file or text pastas, this will point to the /upload endpoint, otherwise to the /url endpoint, essentially directly taking the user to the url stored in the pasta
let svg: String = match pastas[index].pasta_type.as_str() {
"url" => misc::string_to_qr_svg(format!("{}/url/{}", &ARGS.public_path, &id).as_str()),
_ => misc::string_to_qr_svg(format!("{}/pasta/{}", &ARGS.public_path, &id).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

View file

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

View file

@ -1,4 +1,4 @@
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_web::{web, HttpResponse, Responder};
use mime_guess::from_path;
use rust_embed::RustEmbed;
@ -19,45 +19,3 @@ fn handle_embedded_file(path: &str) -> HttpResponse {
async fn static_resources(path: web::Path<String>) -> impl Responder {
handle_embedded_file(path.as_str())
}
// #[derive(Template)]
// #[template(path = "water.css", escape = "none")]
// struct WaterCSS<'a> {
// _marker: PhantomData<&'a ()>,
// }
// // #[derive(Template)]
// // #[template(path = "logo.png", escape = "none")]
// struct LogoPNG<'a> {
// _marker: PhantomData<&'a ()>,
// }
// #[derive(Template)]
// #[template(path = "favicon.svg", escape = "none")]
// struct Favicon<'a> {
// _marker: PhantomData<&'a ()>,
// }
// #[get("/static/{resource}")]
// pub async fn static_resources(resource_id: web::Path<String>) -> HttpResponse {
// match resource_id.into_inner().as_str() {
// "water.css" => HttpResponse::Ok().content_type("text/css").body(
// WaterCSS {
// _marker: Default::default(),
// }
// .render()
// .unwrap(),
// ),
// "logo.png" => HttpResponse::Ok()
// .content_type("image/png")
// .body(Ok(EmbedFile::open("templates/logo.png")?)),
// "favicon.svg" => HttpResponse::Ok().content_type("image/svg+xml").body(
// Favicon {
// _marker: Default::default(),
// }
// .render()
// .unwrap(),
// ),
// _ => HttpResponse::NotFound().content_type("text/html").finish(),
// }
// }

View file

@ -2,10 +2,12 @@ extern crate core;
use crate::args::ARGS;
use crate::endpoints::{
create, edit, errors, info, pasta as pasta_endpoint, pastalist, qr, remove, static_resources,
admin, auth_admin, auth_upload, create, edit, errors, file, guide, list,
pasta as pasta_endpoint, qr, remove, static_resources,
};
use crate::pasta::Pasta;
use crate::util::dbio;
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;
@ -22,19 +24,29 @@ pub mod pasta;
pub mod util {
pub mod animalnumbers;
pub mod auth;
pub mod dbio;
pub mod db;
pub mod db_json;
#[cfg(feature = "default")]
pub mod db_sqlite;
pub mod hashids;
pub mod misc;
pub mod syntaxhighlighter;
pub mod telemetry;
pub mod version;
pub mod http_client;
}
pub mod endpoints {
pub mod admin;
pub mod auth_admin;
pub mod auth_upload;
pub mod create;
pub mod edit;
pub mod errors;
pub mod info;
pub mod file;
pub mod guide;
pub mod list;
pub mod pasta;
pub mod pastalist;
pub mod qr;
pub mod remove;
pub mod static_resources;
@ -65,45 +77,76 @@ async fn main() -> std::io::Result<()> {
ARGS.port.to_string()
);
match fs::create_dir_all("./pasta_data/public") {
match fs::create_dir_all(format!("{}/public", ARGS.data_dir)) {
Ok(dir) => dir,
Err(error) => {
log::error!(
"Couldn't create data directory ./pasta_data/public/: {:?}",
"Couldn't create data directory {}/attachments/: {:?}",
ARGS.data_dir,
error
);
panic!(
"Couldn't create data directory ./pasta_data/public/: {:?}",
error
"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(create::index)
.service(info::info)
.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(actix_files::Files::new("/file", "./pasta_data/public/"))
.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::remove)
.service(pastalist::list)
.service(remove::post_remove)
.service(list::list)
.service(create::index_with_status)
.wrap(Condition::new(
ARGS.auth_username.is_some(),
ARGS.auth_basic_username.is_some()
&& ARGS.auth_basic_username.as_ref().unwrap().trim() != "",
HttpAuthentication::basic(util::auth::auth_validator),
))
})

View file

@ -10,7 +10,7 @@ use crate::util::animalnumbers::to_animal_names;
use crate::util::hashids::to_hashids;
use crate::util::syntaxhighlighter::html_highlight;
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
pub struct PastaFile {
pub name: String,
pub size: ByteSize,
@ -30,16 +30,41 @@ impl PastaFile {
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)]
#[derive(Serialize, Deserialize, Debug)]
pub struct Pasta {
pub id: u64,
pub content: 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,
@ -57,22 +82,36 @@ impl Pasta {
}
}
pub fn created_as_string(&self) -> String {
let date = Local.timestamp(self.created, 0);
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
date.day(),
date.hour(),
date.minute(),
)
pub fn has_file(&self) -> bool {
self.file.is_some()
}
pub fn expiration_as_string(&self) -> String {
if self.expiration == 0 {
String::from("Never")
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 {
let date = Local.timestamp(self.expiration, 0);
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 {
Local.timestamp_opt(self.created, 0).map(|date| {
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
@ -80,6 +119,28 @@ impl Pasta {
date.hour(),
date.minute(),
)
}).earliest().unwrap_or_else(|| {
log::error!("Failed to process created date");
String::from("Unknow")
})
}
pub fn expiration_as_string(&self) -> String {
if self.expiration == 0 {
String::from("Never")
} else {
Local.timestamp_opt(self.expiration, 0).map(|date| {
format!(
"{:02}-{:02} {:02}:{:02}",
date.month(),
date.day(),
date.hour(),
date.minute(),
)
}).earliest().unwrap_or_else(|| {
log::error!("Failed to process expiration");
String::from("Never")
})
}
}
@ -118,7 +179,45 @@ impl Pasta {
};
// it's less than 1 second?????
return String::from("just now");
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 {
@ -132,7 +231,7 @@ impl Pasta {
} as i64;
// get seconds since last read and convert it to days
return ((timenow - self.last_read) / 86400) as u16;
((timenow - self.last_read) / 86400) as u16
}
pub fn content_syntax_highlighted(&self) -> String {
@ -144,7 +243,14 @@ impl Pasta {
}
pub fn content_escaped(&self) -> String {
self.content.replace("`", "\\`").replace("$", "\\$")
html_escape::encode_text(
&self
.content
.replace('\\', "\\\\")
.replace('`', "\\`")
.replace('$', "\\$"),
)
.to_string()
}
}

View file

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

View file

@ -1,29 +1,38 @@
use actix_multipart::Multipart;
use actix_web::dev::ServiceRequest;
use actix_web::web::Bytes;
use actix_web::{error, Error};
use actix_web_httpauth::extractors::basic::BasicAuth;
use futures::TryStreamExt;
use crate::args::ARGS;
pub async fn auth_validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
// check if username matches
if credentials.user_id().as_ref() == ARGS.auth_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."))
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.");
}

View file

@ -1,56 +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> = 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()
}
}
}

View file

@ -13,6 +13,6 @@ 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.get(0).ok_or("No ID found in 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()
}

View file

@ -1,11 +1,15 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::args::ARGS;
use linkify::{LinkFinder, LinkKind};
use magic_crypt::{new_magic_crypt, MagicCryptTrait};
use qrcode_generator::QrCodeEcc;
use std::fs;
use std::fs::{self, File};
use std::io::{BufReader, Read, Write};
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::{dbio, Pasta};
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
@ -31,10 +35,14 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
// 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!(
"./pasta_data/public/{}/{}",
"{}/attachments/{}/{}",
ARGS.data_dir,
p.id_as_animals(),
file.name()
))
@ -44,15 +52,19 @@ pub fn remove_expired(pastas: &mut Vec<Pasta>) {
}
// and remove the containing directory
if fs::remove_dir(format!("./pasta_data/public/{}/", p.id_as_animals())).is_err() {
if fs::remove_dir(format!(
"{}/attachments/{}/",
ARGS.data_dir,
p.id_as_animals()
))
.is_err()
{
log::error!("Failed to delete directory {}!", file.name())
}
}
false
}
});
dbio::save_to_file(pastas);
}
pub fn string_to_qr_svg(str: &str) -> String {
@ -64,3 +76,75 @@ pub fn is_valid_url(url: &str) -> bool {
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

@ -11,7 +11,7 @@ pub fn html_highlight(text: &str, extension: &str) -> String {
let syntax = ps
.find_syntax_by_extension(extension)
.or(Option::from(ps.find_syntax_plain_text()))
.or_else(|| Option::from(ps.find_syntax_plain_text()))
.unwrap();
let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]);
@ -33,5 +33,5 @@ pub fn html_highlight(text: &str, extension: &str) -> String {
highlighted_content2 =
highlighted_content2.replace("style=\"color:#183691;\"", "style=\"color:blue;\"");
return highlighted_content2;
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)
}

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

@ -1,5 +0,0 @@
<svg width="63" height="73" viewBox="0 0 63 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.806818 72.1932L12.7045 0.636362H25.0455L19.9659 30.9773C19.6023 33.3409 19.7955 35.4205 20.5455 37.2159C21.3182 38.9886 22.5682 40.3864 24.2955 41.4091C26.0227 42.4091 28.1364 42.9091 30.6364 42.9091C33.1591 42.9091 35.4432 42.4091 37.4886 41.4091C39.5568 40.3864 41.2614 38.9886 42.6023 37.2159C43.9432 35.4205 44.7955 33.3409 45.1591 30.9773L50.2386 0.636362H62.6136L53.8864 53H41.8864L43.2159 45.4318H42.8068C41.1932 47.8409 39.1591 49.7159 36.7045 51.0568C34.25 52.375 31.6477 53.0341 28.8977 53.0341C26.2386 53.0341 23.8864 52.375 21.8409 51.0568C19.8182 49.7159 18.4205 47.8409 17.6477 45.4318H17.2386L12.7727 72.1932H0.806818Z" fill="#868686"/>
<path d="M0.806818 72.1932L12.7045 0.636362H25.0455L19.9659 30.9773C19.6023 33.3409 19.7955 35.4205 20.5455 37.2159C21.3182 38.9886 22.5682 40.3864 24.2955 41.4091C26.0227 42.4091 28.1364 42.9091 30.6364 42.9091C33.1591 42.9091 35.4432 42.4091 37.4886 41.4091C39.5568 40.3864 41.2614 38.9886 42.6023 37.2159C43.9432 35.4205 44.7955 33.3409 45.1591 30.9773L50.2386 0.636362H62.6136L53.8864 53H41.8864L43.2159 45.4318H42.8068C41.1932 47.8409 39.1591 49.7159 36.7045 51.0568C34.25 52.375 31.6477 53.0341 28.8977 53.0341C26.2386 53.0341 23.8864 52.375 21.8409 51.0568C19.8182 49.7159 18.4205 47.8409 17.6477 45.4318H17.2386L12.7727 72.1932H0.806818Z" fill="#868686"/>
<path d="M0.806818 72.1932L12.7045 0.636362H25.0455L19.9659 30.9773C19.6023 33.3409 19.7955 35.4205 20.5455 37.2159C21.3182 38.9886 22.5682 40.3864 24.2955 41.4091C26.0227 42.4091 28.1364 42.9091 30.6364 42.9091C33.1591 42.9091 35.4432 42.4091 37.4886 41.4091C39.5568 40.3864 41.2614 38.9886 42.6023 37.2159C43.9432 35.4205 44.7955 33.3409 45.1591 30.9773L50.2386 0.636362H62.6136L53.8864 53H41.8864L43.2159 45.4318H42.8068C41.1932 47.8409 39.1591 49.7159 36.7045 51.0568C34.25 52.375 31.6477 53.0341 28.8977 53.0341C26.2386 53.0341 23.8864 52.375 21.8409 51.0568C19.8182 49.7159 18.4205 47.8409 17.6477 45.4318H17.2386L12.7727 72.1932H0.806818Z" fill="#868686"/>
</svg>

Before

Width:  |  Height:  |  Size: 2 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

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

View file

@ -1,20 +1,40 @@
{% include "header.html" %}
<form method="POST" enctype="multipart/form-data">
<form action="/{{ path }}/{{ pasta.id_as_animals() }}" method="POST" enctype="multipart/form-data">
<h4>
Editing pasta '{{ pasta.id_as_animals() }}'
Editing upload '{{ pasta.id_as_animals() }}'
</h4>
<label>Content</label>
<br>
<textarea style="width: 100%; min-height: 100px" name="content" id="content" autofocus>{{ pasta.content }}</textarea>
<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 %}
{% if args.readonly %}
<input style="width: 140px; background-color: limegreen" disabled type="submit" value="Read Only"/>
{%- else %}
<input style="width: 140px; background-color: limegreen" type="submit" value="Save"/>
{%- endif %}
</td>
<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" %}
{% include "footer.html" %}

View file

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

View file

@ -1,17 +1,15 @@
{% if !args.hide_footer %}
<hr>
<p style="font-size: smaller">
<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() }}
{%- endif %}
<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

@ -10,58 +10,53 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="{{ args.public_path }}/static/favicon.svg">
{% if !args.pure_html %}
{% if args.custom_css.as_ref().is_none() %}
<link rel="stylesheet" href="{{ args.public_path }}/static/water.css">
<link rel="icon" type="image/svg+xml" href="{{ args.public_path_as_str() }}/static/favicon.ico">
<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 %}
{%- endif %} {%- endif %}
</head>
{% if args.wide %}
<body style="
max-width: 1080px;
margin: auto;
padding-left:0.5rem;
padding-right:0.5rem;
line-height: 1.5;
font-size: 1.1em;
">
<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 %}
<body style="
max-width: 800px;
margin: auto;
padding-left:0.5rem;
padding-right:0.5rem;
line-height: 1.5;
font-size: 1.1em;
">
<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 %}
<b style="margin-right: 0.5rem">
<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>
{% if !args.hide_logo %}
<!-- <i><span style="font-size:2.2rem; margin-right:1rem">μ</span></i> -->
<img width=26 style="margin-bottom: -6px; margin-right: 0.5rem;"
src="{{ args.public_path }}/static/logo.png">
{%- endif %} {% if args.title.as_ref().is_none() %}
MicroBin {%- else %} {{ args.title.as_ref().unwrap() }} {%- endif %}
</b>
<a href="{{ args.public_path_as_str() }}/" style="margin-right: 0.5rem;
margin-left: 0.5rem">New</a>
<a href="{{ args.public_path }}/" style="margin-right: 0.5rem; margin-left: 0.5rem">New
</a>
{% 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 }}/pastalist" style="margin-right: 0.5rem; margin-left: 0.5rem">List</a>
<a href="{{ args.public_path_as_str() }}/guide" style="margin-right: 0.5rem;
margin-left: 0.5rem">Guide</a>
<a href="{{ args.public_path }}/info" style="margin-right: 0.5rem; margin-left: 0.5rem">Info</a>
<hr>
</div>
<!-- <hr> -->
{%- endif %}

View file

@ -1,111 +1,112 @@
{% include "header.html" %}
<form id="pasta-form" action="upload" method="POST" enctype="multipart/form-data">
<br>
<div id="settings">
<div>
<label for="expiration">Expiration</label><br>
<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
{%- endif %} 1 minute
</option>
{% if args.default_expiry == "10min" %}
<option selected value="10min">
{%- else %}
<option value="10min">
{%- endif %}
10 minutes
{%- endif %} 10 minutes
</option>
{% if args.default_expiry == "1hour" %}
<option selected value="1hour">
{%- else %}
<option value="1hour">
{%- endif %}
1 hour
{%- endif %} 1 hour
</option>
{% if args.default_expiry == "24hour" %}
<option selected value="24hour">
{%- else %}
<option value="24hour">
{%- endif %}
24 hours
{%- 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
{%- endif %} 1 week
</option>
</optgroup>
{% if !args.no_eternal_pasta %}
<option value="never">Never Expire</option>
{% 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="expiration">Burn After</label><br>
<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
{%- endif %} First Read
</option>
{% if args.default_burn_after == 10 %}
<option selected value="10">
{%- else %}
<option value="10">
{%- endif %}
10th Read
{%- endif %} 10th Read
</option>
{% if args.default_burn_after == 100 %}
<option selected value="100">
{%- else %}
<option value="100">
{%- endif %}
100th Read
{%- endif %} 100th Read
</option>
{% if args.default_burn_after == 1000 %}
<option selected value="1000">
{%- else %}
<option value="1000">
{%- endif %}
1000th Read
{%- endif %} 1000th Read
</option>
{% if args.default_burn_after == 10000 %}
<option selected value="10000">
{%- else %}
<option value="10000">
{%- endif %}
10000th Read
{%- endif %} 10000th Read
</option>
</optgroup>
{% if args.default_burn_after == 0 %}
<option selected value="0">
{%- else %}
<option value="0">
{%- endif %}
No Limit
</option>
</select>
</div>
{%- endif %}
{% if args.highlightsyntax %}
<div>
<label for="syntax-highlight">Syntax</label><br>
<select style="width: 100%;" name="syntax-highlight" id="syntax-highlight">
<label for="syntax_highlight">Syntax <sup> <a href="/guide#syntax"></a></sup></label><br>
<select style="width: 100%;" name="syntax_highlight" id="syntax_highlight">
<option value="none">None</option>
<optgroup label="Source Code">
<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>
@ -127,69 +128,259 @@
<option value="rb">Ruby</option>
<option value="sc">Scala</option>
<option value="swift">Swift</option>
</optgroup>
<optgroup label="Descriptors">
<!-- 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">
<input type="hidden" name="syntax_highlight" value="none">
{%- endif %}
{% if args.encryption_client_side || args.encryption_server_side || args.enable_readonly || args.private %}
<div>
{% if args.editable || args.private %}
<label>Other</label>
{%- endif %}
{% if args.editable %}
<div>
<input type="checkbox" id="editable" name="editable" value="editable">
<label for="editable">Editable</label>
</div>
{%- endif %}
{% if args.private %}
<div>
<input type="checkbox" id="private" name="private" value="private">
<label for="private">Private</label>
</div>
{%- endif %}
<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>
<textarea style="width: 100%; min-height: 100px; margin-bottom: 2em" name="content" autofocus></textarea>
<div style="overflow:auto;">
<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 style="float: left">
<label for="file" id="attach-file-button-label"><a role="button" id="attach-file-button">Select or drop
file attachment</a></label>
<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 %}
{% if args.readonly %}
<b>
<input style="width: 140px; float: right; background-color: #0076d18f;" disabled type="submit"
value="Read Only" /></b>
{%- else %}
<b>
<input style="width: 140px; float: right; background-color: #0076d18f;" type="submit" value="Save" />
<input style="width: 140px; float: right; background-color:
#2975D2; color: white;" id="submit-button" type="submit" value="Save" />
{% if args.readonly %}
{% if status == "incorrect" %}
<input style="width: 160px; float: right; background-color: rgba(255, 0, 0, 0.137);" type="password"
id="uploader_password" name="uploader_password" placeholder="Incorrect password!" />
{% else %}
<input style="width: 160px; float: right;" type="password" id="uploader_password" name="uploader_password"
placeholder="Uploader Password" />
{% endif %}
{% endif %}
</b>
{%- endif %}
</div>
<input type="hidden" name="content" id="content">
<input type="hidden" name="encrypt_client" id="encrypt_client">
{% if args.encryption_server_side || 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 hiddenFileButton = document.getElementById('file');
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) {
@ -208,6 +399,7 @@
attachFileButton.textContent = "Attached: " + hiddenFileButton.files[0].name;
evt.preventDefault();
};
// {%- endif %}
</script>
@ -218,25 +410,39 @@
#settings {
display: grid;
grid-gap: 6px;
grid-template-columns: repeat(auto-fit, 150px);
grid-gap: 10px;
grid-template-columns: repeat(auto-fit, 152px);
grid-template-rows: repeat(1, 90px);
margin-bottom: 0.5rem;
margin-bottom: 1rem;
}
select {
height: 3rem;
}
/* {% if !args.pure_html %} */
#attach-file-button-label {
padding-top: 1rem;
padding-bottom: 1rem;
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" %}
{% include "footer.html" %}

View file

@ -1,42 +0,0 @@
{% include "header.html" %}
<h2>Welcome to MicroBin</h2>
<div style="height: 200px;">
<div style="float: left">
<h4>Links</h4>
<a href="https://microbin.eu/documentation" style="margin-right: 1rem">Documentation and Help</a>
<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>Pastas</b></td>
<td>{{pastas.len()}} </td>
</tr>
</table>
</div>
</div>
{% if message != "" %}
<h4>Messages</h4>
<p>{{message}}</p>
{%- endif %}
<br>
{% 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,145 +0,0 @@
{% include "header.html" %}
<div style="float: left">
{% if pasta.content != "No Text Content" %}
<button id="copy-text-button" class="copy-button" style="margin-right: 0.5rem">
Copy Text
</button>
{% if args.public_path.to_string() != "" && pasta.pasta_type == "url" %}
<button id="copy-redirect-button" class="copy-button" style="margin-right: 0.5rem">
Copy Redirect
</button>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/raw/{{pasta.id_as_animals()}}">Raw Text
Content</a>
{%- endif %}
{% if args.qr && args.public_path.to_string() != "" %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/qr/{{pasta.id_as_animals()}}">QR</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a style="margin-right: 1rem" href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</div>
<div style="float: right">
<a style="margin-right: 0.5rem"
href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}"><i>{{pasta.id_as_animals()}}</i></a>
{% if args.public_path.to_string() != "" %}
<button id="copy-url-button" class="copy-button" style="margin-right: 0">
Copy URL
</button>
{%- endif %}
</div>
{% if pasta.file.is_some() %}
<br>
<br>
<a href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}" download>
Download attached file: '{{pasta.file.as_ref().unwrap().name()}}' [{{pasta.file.as_ref().unwrap().size}}]
</a>
{%- endif %}
<br>
<br>
{% if pasta.content != "No Text Content" %}
<div class="code-container">
<div style="clear: both;">
{% 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 %}
<div>
{% 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 %}
</div>
<br>
<script>
const copyURLBtn = document.getElementById("copy-url-button")
const copyTextBtn = document.getElementById("copy-text-button")
const copyRedirectBtn = document.getElementById("copy-redirect-button")
const content = `{{ pasta.content_escaped() }}`
const url = `{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}`
const redirect_url = `{{ args.public_path }}/url/{{pasta.id_as_animals()}}`
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)
})
}
copyTextBtn.addEventListener("click", () => {
navigator.clipboard.writeText(content)
copyTextBtn.innerHTML = "Copied"
setTimeout(() => {
copyTextBtn.innerHTML = "Copy Text"
}, 1000)
})
</script>
<style>
code-line {
counter-increment: listing;
text-align: right;
float: left;
clear: left;
}
code-line::before {
content: counter(listing);
display: inline-block;
padding-left: auto;
margin-left: auto;
text-align: left;
width: 1.6rem;
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;
}
.code-container {
position: relative;
}
.hidden {
display: none;
}
.copy-button {
font-size: small;
padding: 4px;
padding-left: 0.8rem;
padding-right: 0.8rem;
}
</style>
{% include "footer.html" %}

View file

@ -1,121 +0,0 @@
{% include "header.html" %}
{% if pastas.is_empty() %}
<br>
<p>
No pastas yet. 😔 Create one <a href="{{ args.public_path }}/">here</a>.
</p>
<br>
{%- else %}
<h3>Pastas</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%; white-space: nowrap;">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<th style="width: 30%">
Key
</th>
<th style="width: 20%">
Created
</th>
<th style="width: 20%">
Expiration
</th>
<th style="width: 30%">
</th>
</thead>
<tbody>
{% for pasta in pastas %}
{% if pasta.pasta_type == "text" && !pasta.private %}
<tr>
<td>
<a href="{{ args.public_path }}/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="{{ args.public_path }}/raw/{{pasta.id_as_animals()}}">Raw</a>
{% if pasta.file.is_some() %}
<a style="margin-right:1rem"
href="{{ args.public_path }}/file/{{pasta.id_as_animals()}}/{{pasta.file.as_ref().unwrap().name()}}">File</a>
{%- endif %}
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
<h3>URL Redirects</h3>
{% if args.pure_html %}
<table border="1" style="width: 100%">
{% else %}
<table style="width: 100%">
{% endif %}
<thead>
<th style="width: 30%">
Key
</th>
<th style="width: 20%">
Created
</th>
<th style="width: 20%">
Expiration
</th>
<th style="width: 30%">
</th>
</thead>
{% for pasta in pastas %}
{% if pasta.pasta_type == "url" && !pasta.private %}
<tr>
<td>
<a href="{{ args.public_path }}/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="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">Open</a>
<a style="margin-right:1rem; cursor: pointer;" id="copy-button"
data-url="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">Copy</a>
{% if pasta.editable %}
<a style="margin-right:1rem" href="{{ args.public_path }}/edit/{{pasta.id_as_animals()}}">Edit</a>
{%- endif %}
<a href="{{ args.public_path }}/remove/{{pasta.id_as_animals()}}">Remove</a>
</td>
</tr>
{%- endif %}
{% endfor %}
</tbody>
</table>
<br>
{%- endif %}
<script>
// const btn = document.getElementById("copy-button")
const btn = document.querySelector("#copy-button");
btn.addEventListener("click", () => {
navigator.clipboard.writeText(btn.dataset.url)
btn.innerHTML = "Copied"
setTimeout(() => {
btn.innerHTML = "Copy"
}, 1000)
})
</script>
{% include "footer.html" %}

View file

@ -1,17 +1,17 @@
{% include "header.html" %}
<div style="float: left">
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">Back to Pasta</a>
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">Back to Upload</a>
</div>
<div style="text-align: center; padding: 3rem;">
{% if pasta.pasta_type == "url" %}
<a href="{{ args.public_path }}/url/{{pasta.id_as_animals()}}">
<a href="{{ args.public_path_as_str() }}/url/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% else %}
<a href="{{ args.public_path }}/pasta/{{pasta.id_as_animals()}}">
<a href="{{ args.public_path_as_str() }}/upload/{{pasta.id_as_animals()}}">
{{qr}}
</a>
{% endif %}

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" %}