diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35d6b38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +target +.cargo +*.sh +*.tar.gz +Paste.toml +upload/**/* +!upload/README diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ff6a7ad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rktpb" +version = "1.0.0" +authors = ["Sergio Benitez "] +edition = "2021" +description = "A pastebin that does just enough to be really useful." +repository = "https://github.com/SergioBenitez/rktpb" +readme = "README.md" +keywords = ["pastebin", "rocket", "server", "markdown", "highlight"] +license = "AGPL-3.0-only" +categories = ["web-programming::http-server"] + +[lints.clippy] +large_enum_variant = "allow" +mutable_key_type = "allow" + +[dependencies] +rocket = "0.5" +rocket_dyn_templates = { version = "0.1", features = ["tera"] } +yansi = "1.0.0-rc" +rand = "0.8" +syntect = "5" +comrak = "0.19" +futures = "0.3" +humantime = "2.1" +humantime-serde = "1.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ca9b055 --- /dev/null +++ b/LICENSE @@ -0,0 +1,619 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/Paste.toml.template b/Paste.toml.template new file mode 100644 index 0000000..d247145 --- /dev/null +++ b/Paste.toml.template @@ -0,0 +1,29 @@ +[default] # default configuration irrespective of compilation mode +id_length = 3 # paste ID length +upload_dir = "upload" # directory to save uploads in +paste_limit = "384KiB" # maximum paste upload file size +max_age = "30 days" # how long a paste is considered fresh +reap_interval = "5 minutes" # how often to reap stale uploads +server_url = "http://127.0.0.1:8000" # URL server is reachable at +address = "127.0.0.1" # address to listen on +port = 8000 # port to listen on +keep_alive = 5 # HTTP keep-alive in seconds +ident = "Rocket" # server `Ident` header +ip_header = "X-Real-IP" # header to inspect for client IP +log_level = "normal" # console log level +cli_colors = true # enable (detect TTY) or disable CLI colors + +[default.cors] # CORS config - one key/value for each allowed host +"https://example.com" = ["options", "get", "post", "delete"] # methods to allow + +[default.shutdown] # settings for graceful shutdown +ctrlc = true # whether `` initiates a shutdown +signals = ["term", "hup"] # signals that initiate a shutdown +grace = 5 # grace period length in seconds +mercy = 5 # mercy period length in seconds + +[debug] # overrides for when application is compiled in debug mode +# key/values are identical to `default` + +[release] # overrides for when application is compiled in release mode +# key/values are identical to `default` diff --git a/README.md b/README.md new file mode 100644 index 0000000..26d4e05 --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +# Rocket Powered Pastebin (`rktpb` | [`paste.rs`]) + +A pastebin that does just enough to be _really_ useful. + + - [x] Really fast, really lightweight. + - [x] Renders _markdown_ like GitHub. + - [x] Highlights `source code`. + - [x] Returns plain text, too. + - [x] Has a simple API usable via CLI. + - [x] Has support for CORS. + - [x] Limits paste upload sizes. + - [x] No database: uses the file system. + - [x] Automatically deletes stale pastes. + +This pastebin powers [`paste.rs`], a public instance. Further usage details can +be found there. + +[`paste.rs`]: https://paste.rs + +## Usage + +**R**ocket **P**owered Paste**b**in (`rktpb`) is written in +[Rust](https://rust-lang.org) with [Rocket](https://rocket.rs). To start the +server, use `cargo`: + +```sh +# clone the repository +git clone https://github.com/SergioBenitez/rktpb + +# change into directory: the `static` folder needs to be in CWD before running +cd rktpb + +# compile and start the server with the default config +cargo run +``` + +## Configuration + +Configuration is provided via [environment variables](#environment-variables) or +a [TOML file](#toml-file). A set of defaults is always provided. + +The complete list of configurable parameters is below: + +| Name | Default Value | Description | +|--------------------|-----------------------------|-------------------------------------------| +| `id_length` | `3` | paste ID length +| `upload_dir` | `"upload"` | directory to save uploads in | +| `paste_limit` | `"384KiB"` | maximum paste upload file size | +| `max_age` | `"30 days"` | how long a paste is considered fresh | +| `reap_interval` | `"5 minutes"` | how often to reap stale uploads | +| `server_url` | `"http://{address}:{port}"` | URL server is reachable at | +| | | | +| `cors.{host}` | `["{HTTP method}"..]` | allow CORS {HTTP methods} for {host} | +| | | | +| `address` | `"127.0.0.1"` | address to listen on | +| `port` | `8000` | port to listen on | +| `keep_alive` | `5` | HTTP keep-alive in seconds | +| `ident` | `"Rocket"` | server `Ident` header | +| `ip_header` | `"X-Real-IP"` | header to inspect for client IP | +| `log_level` | `"normal"` | console log level | +| `cli_colors` | `true` | enable (detect TTY) or disable CLI colors | +| | | | +| `shutdown.ctrlc` | `true` | whether `` initiates a shutdown | +| `shutdown.signals` | `["term", "hup"]` | signals that initiate a shutdown | +| `shutdown.grace` | `5` | grace period length in seconds | +| `shutdown.mercy` | `5` | mercy period length in seconds | + +You'll definitely want to configure the values in the first two categories, from +`id_length` to `cors`. + +You should likely use the defaults for the rest. + +### Environment Variables + +Use an environment variable name equivalent to the parameter name prefixed with +`PASTE_`: + +```sh +PASTE_ID_LENGTH=10 PASTE_MAX_AGE="1 year" ./rktpb +``` + +To set structured data via environment variables, such as CORS, use [TOML-like +syntax](https://docs.rs/figment/latest/figment/providers/struct.Env.html): + +```sh +PASTE_CORS='{"http://example.com"=["get", "post"]}' ./rktpb +``` + +### TOML File + +See [`Paste.toml.template`](Paste.toml.template) for a template with all of the +defaults set as well as a dummy `cors` configuration for `http://example.com` +that allows the `options`, `get`, `post`, and `delete` HTTP methods. + +```sh +mv Paste.toml.template Paste.toml +``` + +By default, the application searches for a file called `Paste.toml` in the CWD. +The path to the file can be overridden by setting `PASTE_CONFIG`. For example, +to use a file named `rktpb.toml`, use `PASTE_CONFIG="rktpb.toml" ./rktpb`. + +## Deploying + +To deploy, build in release mode and ship/run the resulting binary along with +`static/`, `templates/`, and any config: + +```sh +# build in release mode for `${TARGET}` +cargo build --release --target ${TARGET} + +# create a tarball of everything that's needed +tar -cvzf "rktpb.tar.gz" \ + Paste.toml static templates \ + -C target/${TARGET}/release rktpb +``` + +However you choose to deploy, you'll need to ensure that the CWD at the time the +server is started contains the `static` and `templates` directories as well as +the config file, if one is used. + +Note that when the server is compiled in `release` mode, the `[release]` section +of a TOML config file can be used to override config values; the same is true +when compiled in `debug` mode with `[debug]`. + +## License + +Rocket Powered Pastebin (`rktpb` | [`paste.rs`]) +Copyright © 2020 Sergio Benitez + +This program is free software: you can redistribute it and/or modify it under +the terms of the [GNU Affero General Public License version 3 (GNU AGPLv3) as +published by the Free Software +Foundation](https://www.gnu.org/licenses/agpl-3.0.en.html#license-text). This +program is distributed in the hope that it will be useful, but **WITHOUT ANY +WARRANTY**; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the GNU AGPLv3 [LICENSE](LICENSE) for more details. + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project shall be licensed under the GNU AGPLv3 License, +without any additional terms or conditions. diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..529ea39 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,106 @@ +use rocket::figment::{Figment, Profile}; +use rocket::figment::providers::{Format, Toml, Serialized, Env}; +use rocket::figment::value::magic::RelativePathBuf; +use rocket::serde::{de, Deserialize, Serialize}; + +use rocket::{Sentinel, Rocket, Ignite, Request}; +use rocket::data::{ByteUnit, ToByteUnit, Limits}; +use rocket::http::{Status, uri::Absolute}; +use rocket::request::{FromRequest, Outcome}; +use rocket::outcome::IntoOutcome; + +use yansi::Paint; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(crate = "rocket::serde")] +pub struct Config { + pub id_length: usize, + pub paste_limit: ByteUnit, + pub server_url: Absolute<'static>, + #[serde(deserialize_with = "directory")] + #[serde(serialize_with = "RelativePathBuf::serialize_original")] + pub upload_dir: RelativePathBuf, +} + +impl Config { + pub fn figment() -> Figment { + #[cfg(debug_assertions)] const DEFAULT_PROFILE: &str = "debug"; + #[cfg(not(debug_assertions))] const DEFAULT_PROFILE: &str = "release"; + + // This the base figment, without our `Config` defaults. + let mut figment = Figment::new() + .join(rocket::Config::default()) + .merge(Toml::file(Env::var_or("PASTE_CONFIG", "Paste.toml")).nested()) + .merge(Env::prefixed("PASTE_").profile(Profile::Global)) + .select(Profile::from_env_or("PASTE_PROFILE", DEFAULT_PROFILE)); + + // Dynamically determine `server_url` default based on address/port. + let default_server_url = match figment.extract::() { + Ok(config) => { + let proto = if config.tls_enabled() { "https" } else { "http" }; + let url = format!("{}://{}:{}", proto, config.address, config.port); + Absolute::parse_owned(url).expect("default URL is Absolute") + }, + Err(_) => uri!("http://127.0.0.1:8017"), + }; + + // Now set the `Config` defaults. + figment = figment + .join(Serialized::defaults(Config { + id_length: 3, + paste_limit: 384.kibibytes(), + server_url: default_server_url, + upload_dir: "upload".into(), + })); + + // Configure Rocket based on `Config` settings. If this fails now, it's + // fine - it'll fail when attached too, so we won't miss out. + if let Ok(config) = figment.extract::() { + figment = figment + .merge((rocket::Config::TEMP_DIR, config.upload_dir)) + .merge((rocket::Config::LIMITS, Limits::default() + .limit("form", config.paste_limit) + .limit("data-form", config.paste_limit) + .limit("file", config.paste_limit) + .limit("string", config.paste_limit) + .limit("bytes", config.paste_limit) + .limit("json", config.paste_limit) + .limit("msgpack", config.paste_limit) + .limit("paste", config.paste_limit), + )); + } + + figment + } +} + +impl Sentinel for Config { + fn abort(rocket: &Rocket) -> bool { + rocket.state::().is_none() + } +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for &'r Config { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + req.rocket().state::().or_error((Status::InternalServerError, ())) + } +} + +fn directory<'de, D: de::Deserializer<'de>>(de: D) -> Result { + let path = RelativePathBuf::deserialize(de)?; + let resolved = path.relative(); + if !resolved.exists() { + let path = resolved.display(); + return Err(de::Error::custom(format!("Path {} does not exist.", path.primary()))); + } + + if !resolved.is_dir() { + let path = resolved.display(); + return Err(de::Error::custom(format!("Path {} is not a directory.", path.primary()))); + } + + Ok(path) +} diff --git a/src/cors.rs b/src/cors.rs new file mode 100644 index 0000000..6abacfa --- /dev/null +++ b/src/cors.rs @@ -0,0 +1,78 @@ +use std::io::Cursor; +use std::collections::HashMap; + +use rocket::{Rocket, Request, Response, Orbit}; +use rocket::fairing::{AdHoc, Fairing, Info, Kind}; +use rocket::http::{Status, Header, Method, uri::Absolute}; +use rocket::serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct Cors { + #[serde(default)] + cors: HashMap, Vec>, +} + +impl Cors { + pub fn fairing() -> impl Fairing { + AdHoc::try_on_ignite("CORS Configuration", |rocket| async { + match rocket.figment().extract::() { + Ok(cors) => Ok(rocket.attach(cors)), + Err(e) => { + let kind = rocket::error::ErrorKind::Config(e); + rocket::Error::from(kind).pretty_print(); + Err(rocket) + }, + } + }) + } +} + +#[rocket::async_trait] +impl Fairing for Cors { + fn info(&self) -> Info { + Info { name: "CORS", kind: Kind::Liftoff | Kind::Response } + } + + async fn on_liftoff(&self, _rocket: &Rocket) { + use yansi::Paint; + + info!("{}{}", "📫 ".mask(), "CORS:".magenta()); + if self.cors.is_empty() { + return info_!("status: {}", "disabled".red()); + } + + info_!("status: {}", "enabled".green()); + for (host, methods) in &self.cors { + info_!("{}: {:?}", host.magenta(), methods.primary()); + } + } + + async fn on_response<'r>(&self, req: &'r Request<'_>, res: &mut Response<'r>) { + let allowed_host_methods = req.headers().get_one("Origin") + .and_then(|origin| Absolute::parse(origin).ok()) + .and_then(|host| self.cors.get_key_value(&host)) + .filter(|(_, methods)| methods.contains(&req.method())); + + if let Some((host, methods)) = allowed_host_methods { + const ALLOW_ORIGIN: &str = "Access-Control-Allow-Origin"; + const ALLOW_METHODS: &str = "Access-Control-Allow-Methods"; + const ALLOW_HEADERS: &str = "Access-Control-Allow-Headers"; + + let mut allow_methods = String::with_capacity(methods.len() * 8); + for (i, method) in methods.iter().enumerate() { + if i != 0 { allow_methods.push(','); } + allow_methods.push_str(method.as_str()); + } + + res.set_header(Header::new(ALLOW_ORIGIN, host.to_string())); + res.set_header(Header::new(ALLOW_METHODS, allow_methods)); + res.set_header(Header::new(ALLOW_HEADERS, "Content-Type")); + + if req.method() == Method::Options && res.status() == Status::NotFound { + res.set_status(Status::Ok); + res.set_sized_body(0, Cursor::new("")); + } + } + } +} diff --git a/src/highlight.rs b/src/highlight.rs new file mode 100644 index 0000000..b3de987 --- /dev/null +++ b/src/highlight.rs @@ -0,0 +1,107 @@ +use std::io; + +use syntect::parsing::SyntaxSet; +use syntect::highlighting::{ThemeSet, Theme}; + +pub const HIGHLIGHT_EXTS: &[&str] = &[ + "Appfile", "Appraisals", "Berksfile", "Brewfile", "C", "Cheffile", "DOT", "Deliverfile", + "Emakefile", "Fastfile", "GNUmakefile", "Gemfile", "Guardfile", "M", "Makefile", + "OCamlMakefile", "PL", "R", "Rakefile", "Rantfile", "Rprofile", "S", "SConscript", + "SConstruct", "Scanfile", "Sconstruct", "Snakefile", "Snapfile", "Thorfile", "Vagrantfile", + "adp", "applescript", "as", "asa", "asp", "bash", "bat", "bib", "bsh", "build", "builder", "c", + "c++", "capfile", "cc", "cgi", "cl", "clisp", "clj", "cls", "cmd", "config.ru", "cp", "cpp", + "cpy", "cs", "css", "css.erb", "css.liquid", "csx", "cxx", "d", "ddl", "di", "diff", "dml", + "dot", "dpr", "dtml", "el", "emakefile", "erb", "erbsql", "erl", "fasl", "fcgi", "fish", + "gemspec", "go", "gradle", "groovy", "gv", "gvy", "gyp", "gypi", "h", "h++", "haml", "hh", + "hpp", "hrl", "hs", "htc", "htm", "html", "html.erb", "hxx", "inc", "inl", "ipp", "irbrc", + "java", "jbuilder", "js", "js.erb", "json", "jsp", "l", "lhs", "lisp", "lsp", "ltx", "lua", + "m", "mak", "make", "makefile", "markdn", "markdown", "matlab", "md", "mdown", "mk", "ml", + "mli", "mll", "mly", "mm", "mud", "opml", "p", "pas", "patch", "php", "php3", "php4", "php5", + "php7", "phps", "phpt", "phtml", "pl", "pm", "pod", "podspec", "prawn", "properties", "pxd", + "pxd.in", "pxi", "pxi.in", "py", "py3", "pyi", "pyw", "pyx", "pyx.in", "r", "rabl", "rails", + "rake", "rb", "rbx", "rd", "re", "rest", "rhtml", "rjs", "rpy", "rs", "rss", "rst", + "ruby.rail", "rxml", "s", "sass", "sbt", "scala", "scm", "sconstruct", "script editor", "sh", + "shtml", "simplecov", "sql", "sql.erb", "ss", "sty", "svg", "t", "tcl", "tex", "textile", + "thor", "tld", "tmpl", "tpl", "txt", "wscript", "xhtml", "xml", "xsd", "xslt", "yaml", "yaws", + "yml", "zsh", +]; + +pub struct Highlighter { + theme: Theme, + syntaxes: SyntaxSet, +} + +impl Highlighter { + pub fn default() -> Option { + let mut reader = io::Cursor::new(include_str!("../static/GitHub.tmTheme")); + Some(Highlighter { + theme: ThemeSet::load_from_reader(&mut reader).ok()?, + syntaxes: SyntaxSet::load_defaults_newlines(), + }) + } + + pub fn contains(ext: &str) -> bool { + HIGHLIGHT_EXTS.binary_search(&ext).is_ok() + } + + pub fn highlight(&self, code: &str, lang: &str) -> io::Result { + let syntax = self.syntaxes.find_syntax_by_token(lang) + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "missing syntax"))?; + + syntect::html::highlighted_html_for_string(code, &self.syntaxes, syntax, &self.theme) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } + + pub fn render_markdown(&self, markdown: &str) -> io::Result { + let options = comrak::ComrakOptions { + extension: { + let mut opts = comrak::ComrakExtensionOptions::default(); + opts.strikethrough = true; + opts.tagfilter = true; + opts.table = true; + opts.autolink = true; + opts.tasklist = true; + opts.superscript = true; + opts.footnotes = true; + opts.front_matter_delimiter = Some("---".into()); + opts.header_ids = Some(String::new()); + opts + }, + parse: Default::default(), + render: { + let mut opts = comrak::ComrakRenderOptions::default(); + opts.github_pre_lang = true; + // NB: we use CSP to ensure no JS leaks. + opts.unsafe_ = true; + opts + }, + }; + + let arena = comrak::Arena::new(); + let ast = comrak::parse_document(&arena, markdown, &options); + self.highlight_ast(ast); + + let mut html = Vec::new(); + comrak::format_html(ast, &options, &mut html)?; + String::from_utf8(html).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } + + fn highlight_ast<'a>(&self, ast: &'a comrak::nodes::AstNode<'a>) { + use comrak::arena_tree::NodeEdge; + use comrak::nodes::{NodeValue, NodeHtmlBlock}; + + for node in ast.traverse() { + if let NodeEdge::Start(node) = node { + let mut data = node.data.borrow_mut(); + if let NodeValue::CodeBlock(ref mut block) = data.value { + if let Ok(highlighted) = self.highlight(&block.literal, &block.info) { + data.value = NodeValue::HtmlBlock(NodeHtmlBlock { + literal: highlighted, + ..Default::default() + }); + } + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3b16174 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,139 @@ +#[macro_use] +extern crate rocket; + +mod paste_id; +mod os_header; +mod highlight; +mod cors; +mod config; +mod reaper; + +use rocket_dyn_templates::{Template, context}; +use rocket::tokio::fs::{self, File}; +use rocket::tokio::io::{self, AsyncReadExt}; + +use rocket::State; +use rocket::form::Form; +use rocket::data::Capped; +use rocket::fairing::AdHoc; +use rocket::response::Redirect; +use rocket::request::FlashMessage; +use rocket::fs::{FileServer, TempFile}; +use rocket::http::{Status, ContentType}; + +use paste_id::PasteId; +use os_header::ClientOs; +use highlight::{Highlighter, HIGHLIGHT_EXTS}; +use config::Config; +use cors::Cors; +use reaper::Reaper; + +#[derive(Responder)] +pub enum Paste { + Highlighted(Template), + Regular(File, ContentType), + Markdown(Template), +} + +#[derive(Debug, FromForm)] +struct PasteForm<'r> { + #[field(validate = len(1..))] + content: Capped>, + #[field(validate = with(|e| Highlighter::contains(e), "unknown extension"))] + ext: &'r str, +} + +#[post("/", data = "")] +async fn upload( + mut paste: Capped>, + config: &Config, +) -> io::Result<(Status, String)> { + let id = PasteId::new(config); + paste.persist_to(id.file_path(config)).await?; + + let paste_uri = uri!(config.server_url.clone(), get(id)); + let status = match paste.is_complete() { + true => Status::Created, + false => Status::PartialContent, + }; + + Ok((status, paste_uri.to_string())) +} + +#[post("/web", data = "
")] +async fn web_form_submit( + mut form: Form>, + config: &Config, +) -> io::Result { + let id = PasteId::with_ext(config, form.ext); + form.content.persist_to(&id.file_path(config)).await?; + Ok(Redirect::to(uri!(get(id)))) +} + +// TODO: Authenticate a delete using some kind of token. +#[delete("/")] +async fn delete(id: PasteId<'_>, config: &Config) -> Option<&'static str> { + fs::remove_file(&id.file_path(config)).await.map(|_| "deleted\n").ok() +} + +#[get("/")] +async fn get( + id: PasteId<'_>, + highlighter: &State, + config: &Config, +) -> io::Result> { + let Ok(mut file) = File::open(&id.file_path(config)).await else { + return Ok(None) + }; + + let paste = match id.ext() { + Some("md" | "mdown" | "markdown") => { + let mut file_contents = String::new(); + file.read_to_string(&mut file_contents).await?; + let content = highlighter.render_markdown(&file_contents)?; + Paste::Markdown(Template::render("markdown", context! { config, id, content })) + } + Some(ext) if Highlighter::contains(ext) => { + let mut file_contents = String::new(); + file.read_to_string(&mut file_contents).await?; + let content = highlighter.highlight(&file_contents, ext)?; + let lines = content.lines().count(); + Paste::Highlighted(Template::render("code", context! { config, id, content, lines })) + } + _ => Paste::Regular(file, id.content_type().unwrap_or(ContentType::Plain)), + }; + + Ok(Some(paste)) +} + +#[get("/")] +fn index(config: &Config, os: Option) -> Template { + let (os, cmd) = match os.map(|os| os.name()) { + Some(os @ "windows") => (os, "PowerShell"), + Some(os @ ("linux" | "darwin")) => (os, "cURL"), + _ => ("unix", "cURL") + }; + + Template::render("index", context! { config, cmd, os }) +} + +#[get("/web")] +fn web_form(config: &Config, flash: Option) -> Template { + Template::render("new", context! { + config, + extensions: HIGHLIGHT_EXTS, + error: flash.as_ref().map(FlashMessage::message), + }) +} + +#[rocket::launch] +fn rocket() -> _ { + rocket::custom(Config::figment()) + .mount("/", routes![index, upload, get, delete, web_form, web_form_submit]) + .mount("/", FileServer::from("static").rank(-20)) + .manage(Highlighter::default().expect("failed to load syntax highlighter")) + .attach(Template::fairing()) + .attach(AdHoc::config::()) + .attach(Reaper::fairing()) + .attach(Cors::fairing()) +} diff --git a/src/os_header.rs b/src/os_header.rs new file mode 100644 index 0000000..dbbdad1 --- /dev/null +++ b/src/os_header.rs @@ -0,0 +1,35 @@ +use rocket::request::{Request, FromRequest, Outcome}; +use rocket::http::Status; + +pub struct ClientOs(&'static str); + +impl ClientOs { + pub fn name(&self) -> &'static str { + self.0 + } +} + +static AGENT_MAP: &[(&str, &str)] = &[ + ("Windows", "windows"), ("OpenBSD", "bsd"), ("SunOS", "sun"), + ("Macintosh", "darwin"), ("Linux", "linux"), ("Mac OS X", "darwin"), + ("Google", "google"), ("Yahoo", "yahoo"), ("Bing", "bing") +]; + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for ClientOs { + type Error = (); + + async fn from_request(req: &'r Request<'_>) -> Outcome { + if let Some(agent) = req.headers().get_one("User-Agent") { + for &(agent_os, os) in AGENT_MAP { + if agent.contains(agent_os) { + return Outcome::Success(ClientOs(os)); + } + } + + return Outcome::Forward(Status::NotFound); + } + + Outcome::Forward(Status::NotFound) + } +} diff --git a/src/paste_id.rs b/src/paste_id.rs new file mode 100644 index 0000000..c185572 --- /dev/null +++ b/src/paste_id.rs @@ -0,0 +1,90 @@ +use std::fmt; +use std::borrow::Cow; +use std::path::{Path, PathBuf}; + +use rand::distributions::DistString; +use rand::{thread_rng, distributions::Alphanumeric}; + +use rocket::http::{RawStr, ContentType, impl_from_uri_param_identity}; +use rocket::http::uri::{self, fmt::UriDisplay}; +use rocket::request::FromParam; +use rocket::serde::{Serialize, Serializer}; + +use crate::Config; + +/// The (id, extension) of the requested paste. +pub struct PasteId<'a>(Cow<'a, str>, Option<&'a str>); + +impl<'a> PasteId<'a> { + /// Generates a new, random paste ID. + pub fn new(config: &Config) -> PasteId<'static> { + PasteId::with_ext(config, None) + } + + /// Randomly generates an ID of the configured length. There are no + /// requirements on `ext`; it used simply as a hint in the responder. + pub fn with_ext>>(config: &Config, ext: E) -> Self { + let id = Alphanumeric.sample_string(&mut thread_rng(), config.id_length); + PasteId(Cow::Owned(id), ext.into()) + } + + /// The extension of the paste ID, if there is any. + pub fn ext(&self) -> Option<&str> { + self.1 + } + + /// The Content-Type of the paste ID based on the extension, if any. + pub fn content_type(&self) -> Option { + fn is_browser_executable(ct: &ContentType) -> bool { + ct.is_html() || ct.is_javascript() || ct.is_css() + } + + match self.ext().and_then(ContentType::from_extension) { + Some(ref ct) if is_browser_executable(ct) => None, + other => other + } + } + + /// Where the paste with this ID should be stored. + pub fn file_path(&self, config: &Config) -> PathBuf { + config.upload_dir.relative().join(Path::new(&*self.0)) + } +} + +impl<'a> FromParam<'a> for PasteId<'a> { + type Error = &'a str; + + fn from_param(param: &'a str) -> Result { + fn valid_id(id: &str) -> bool { + id.chars().all(char::is_alphanumeric) + } + + let (id, ext) = RawStr::new(param).split_at_byte(b'.'); + if !valid_id(id.as_str()) { + return Err(param); + } + + let (id, ext) = (id.as_str(), (!ext.is_empty()).then(|| ext.as_str())); + Ok(PasteId(id.into(), ext)) + } +} + +impl Serialize for PasteId<'_> { + fn serialize(&self, ser: S) -> Result { + ser.serialize_str(&self.0) + } +} + +impl UriDisplay for PasteId<'_> { + fn fmt(&self, f: &mut uri::fmt::Formatter<'_, uri::fmt::Path>) -> fmt::Result { + self.0.fmt(f)?; + if let Some(ext) = self.1 { + f.write_raw(".")?; + ext.fmt(f)?; + } + + Ok(()) + } +} + +impl_from_uri_param_identity!([uri::fmt::Path] ('a) PasteId<'a>); diff --git a/src/reaper.rs b/src/reaper.rs new file mode 100644 index 0000000..5aa3a3d --- /dev/null +++ b/src/reaper.rs @@ -0,0 +1,122 @@ +use std::io; +use std::time::Duration; +use std::path::PathBuf; + +use rocket::tokio; + +use rocket::{Rocket, Orbit}; +use rocket::serde::Deserialize; +use rocket::fairing::{Kind, Fairing, AdHoc, Info}; + +/// Default max age is 30 days. +const DEFAULT_MAX_AGE: Duration = Duration::from_secs(30 * 24 * 60 * 60); + +/// Default repeat interval is 5 minutes. +const DEFAULT_INTERVAL: Duration = Duration::from_secs(5 * 60); + +/// Minimum allowed interval is 30 seconds. +const MIN_INTERVAL: Duration = Duration::from_secs(30); + +#[derive(Debug, Deserialize, Clone)] +#[serde(crate = "rocket::serde")] +pub struct Reaper { + #[serde(default, with = "humantime_serde")] + max_age: Option, + #[serde(default, with = "humantime_serde")] + reap_interval: Option, + upload_dir: PathBuf, +} + +impl Reaper { + pub fn fairing() -> impl Fairing { + AdHoc::try_on_ignite("Reaper Config", |rocket| async { + match rocket.figment().extract::() { + Ok(reaper) => Ok(rocket.attach(reaper)), + Err(e) => { + let kind = rocket::error::ErrorKind::Config(e); + rocket::Error::from(kind).pretty_print(); + Err(rocket) + }, + } + }) + } + + async fn reap(&self) -> io::Result<(usize, usize, usize)> { + let mut files_reaped = 0; + let mut files_checked = 0; + let mut files_ok = 0; + let mut entries = tokio::fs::read_dir(&self.upload_dir).await?; + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + let file_name = match path.file_name() { + Some(file_name) => file_name.to_string_lossy(), + None => continue, + }; + + let metadata = entry.metadata().await?; + if !metadata.is_file() || path.extension().is_some() || file_name.starts_with('.') { + continue; + } + + let last_access = metadata.accessed()?; + let elapsed = last_access.elapsed().unwrap_or(Duration::MAX); + if elapsed >= self.max_age() { + files_reaped += tokio::fs::remove_file(&path).await.is_ok() as usize; + } else { + files_ok += 1; + } + + files_checked += 1; + } + + Ok((files_reaped, files_checked, files_ok)) + } + + fn max_age(&self) -> Duration { + self.max_age.unwrap_or(DEFAULT_MAX_AGE) + } + + fn interval(&self) -> Duration { + let base = self.reap_interval.unwrap_or(DEFAULT_INTERVAL); + std::cmp::max(MIN_INTERVAL, base) + } +} + +#[rocket::async_trait] +impl Fairing for Reaper { + fn info(&self) -> rocket::fairing::Info { + Info { name: "Reaper", kind: Kind::Liftoff } + } + + async fn on_liftoff(&self, _rocket: &Rocket) { + use yansi::Paint; + + let icon = "💀 ".mask(); + info!("{}{}", icon, "Reaper:".magenta()); + info_!("status: {}", "enabled".green()); + info_!("directory: {}", self.upload_dir.display().magenta()); + info_!("max age: {}", humantime::format_duration(self.max_age()).magenta()); + info_!("interval: {}", humantime::format_duration(self.interval()).magenta()); + + if let Some(i) = self.reap_interval { + if i < MIN_INTERVAL { + let f = humantime::format_duration(i); + warn_!("note: interval minimum is 30s (was {})", f.magenta()); + } + } + + let reaper = self.clone(); + tokio::spawn(async move { + loop { + match reaper.reap().await { + Err(e) => warn!("Error encountered while reaping: {}", e), + Ok((r, n, k)) => { + info!("{}reaper: {}/{} files reaped ({} fresh)", icon, r, n, k) + } + } + + tokio::time::sleep(reaper.interval()).await; + } + }); + } +} diff --git a/static/GitHub.tmtheme b/static/GitHub.tmtheme new file mode 100644 index 0000000..4b827c8 --- /dev/null +++ b/static/GitHub.tmtheme @@ -0,0 +1,459 @@ + + + + + author + Sergey Makinen + colorSpaceName + sRGB + gutterSettings + + background + #f6f8fa + divider + #EEEEEE + foreground + #B0B0B0 + + name + GitHub 3 + semanticClass + theme.light.git_hub_3 + settings + + + settings + + background + #f6f8fa + caret + #000000 + foreground + #000000 + invisibles + #000000 + lineHighlight + #f6ecb9 + selection + #add2fc + + + + name + Comment + scope + comment + settings + + foreground + #969896 + + + + name + String + scope + string, string support, string keyword + settings + + foreground + #032f62 + + + + name + Built-in constant + scope + constant.language + settings + + foreground + #0086b3 + + + + name + User-defined constant + scope + constant.other + settings + + foreground + #005cc5 + + + + name + Variable + scope + variable + settings + + fontStyle + + + + + name + Operators (shell) + scope + source.shell keyword.operator.assign + settings + + foreground + #000000 + + + + name + Keyword + scope + keyword + settings + + foreground + #d73a49 + + + + name + Keyword + scope + keyword.operator.arithmetic.shell + settings + + foreground + #d73a49 + + + + name + Keyword (Apache) + scope + keyword.core.apacheconf + settings + + foreground + #005cc5 + + + + name + Storage + scope + storage, support.type.return, support.type.receiver + settings + + fontStyle + + foreground + #d73a49 + + + + name + Class name + scope + entity.name.class, entity.section.ini, entity.name.section, source.ts entity.name.type, support.constant.dom.js, entity.name.type.class, entity.name.type.interface + settings + + foreground + #6f42c1 + + + + name + Inherited class + scope + entity.other.inherited-class + settings + + foreground + #6f42c1 + + + + name + Function name + scope + entity.name.function, variable.function.js + settings + + fontStyle + + foreground + #6f42c1 + + + + name + Function argument + scope + variable.parameter + settings + + foreground + #000000 + + + + name + Function argument (Python) + scope + meta.function-call.python variable.parameter.python + settings + + foreground + #e36209 + + + + name + Tag name + scope + entity.name.tag, entity.tag.apacheconf + settings + + fontStyle + + foreground + #22863a + + + + name + Tag name - doctype (HTML) + scope + entity.name.tag.doctype + settings + + fontStyle + + foreground + #000000 + + + + name + Tag attribute + scope + entity.other.attribute-name, entity.other.pseudo-element + settings + + fontStyle + + foreground + #795da3 + + + + name + Library function + scope + support.function, variable.language.this.js, variable.language.js, variable.language.python, support.variable.magic.python, variable.language.this.ts, meta.function-call.js + settings + + fontStyle + + foreground + #005cc5 + + + + name + Property (CSS) + scope + source.css support.type, support.constant.color, constant.codepoint-range.css, support.constant.unicode-range.prefix.css + settings + + foreground + #005cc5 + + + + name + Library constant + scope + support.constant + settings + + fontStyle + + foreground + #005cc5 + + + + name + Library class/type + scope + support.type, support.other.namespace + settings + + foreground + #005cc5 + + + + name + Library class (JS) + scope + source.js support.type + settings + + foreground + #6f42c1 + + + + name + Library class + scope + support.class + settings + + foreground + #005cc5 + + + + name + Library variable + scope + support.other.variable + settings + + fontStyle + + + + + name + Invalid + scope + invalid + settings + + background + #b31d28 + fontStyle + + foreground + #fafbfc + + + + name + CSS property + scope + meta.property-name.css, meta.property-value.css support, meta.property-value.css support.constant, meta.property-value.css + settings + + foreground + #005cc5 + + + + name + CSS property value punctuation + scope + meta.property-value.css punctuation + settings + + foreground + #000000 + + + + name + Constant in string + scope + string constant + settings + + foreground + #183691 + + + + name + Entity (HTML) + scope + constant.character.entity + settings + + foreground + #005cc5 + + + + name + Number + scope + constant.numeric + settings + + foreground + #005cc5 + + + + name + Constant + scope + constant.language + settings + + foreground + #005cc5 + + + + name + Label + scope + entity.name.label + settings + + foreground + #6f42c1 + + + + name + Variables (Go, Python, etc.) + scope + variable.other.exported.go, entity.name.type.exported.go, variable.parameter.go, variable.other.receiver.go, variable.parameter.function.keyword.python, variable.parameter.ts + settings + + foreground + #e36209 + + + + name + Black variables (Go) + scope + support.type.go + settings + + foreground + #000000 + + + + name + Parameter (CSS) + scope + variable.parameter.misc.css + settings + + foreground + #e36209 + + + + uuid + 96D817F0-8536-11E4-B4A9-0800200C9A66 + + diff --git a/static/code.css b/static/code.css new file mode 100644 index 0000000..86124bd --- /dev/null +++ b/static/code.css @@ -0,0 +1,54 @@ +.code-body { + color: #24292e; + box-sizing: border-box; + min-width: 200px; + max-width: 980px; + margin: 0 auto; + padding: 45px; +} + +.code-body strong { + font-weight: 600; +} + +@media (max-width: 767px) { + .code-body { + padding: 15px; + } +} + +.code-body td, +.code-body th { + padding: 0; +} + +.code-body td.linenos { + color: #6e7781; + text-align: right; +} + +.code-body td.code { + width: 100%; +} + +.code-body td.code pre { + padding-left: 10px; +} + +.code-body table { + border-spacing: 0; + border-collapse: collapse; + margin: 0; +} + +.code-body pre { + margin: 0; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + word-wrap: normal; + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; +} diff --git a/static/code.min.css b/static/code.min.css new file mode 100644 index 0000000..f9309d0 --- /dev/null +++ b/static/code.min.css @@ -0,0 +1 @@ +.code-body{color:#24292e;box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}.code-body strong{font-weight:600}@media (max-width:767px){.code-body{padding:15px}}.code-body td,.code-body th{padding:0}.code-body td.linenos{color:#6e7781;text-align:right}.code-body td.code{width:100%}.code-body td.code pre{padding-left:10px}.code-body table{border-spacing:0;border-collapse:collapse;margin:0}.code-body pre{margin:0;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;word-wrap:normal;padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px} diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png new file mode 100644 index 0000000..c8e337a Binary files /dev/null and b/static/favicon-16x16.png differ diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png new file mode 100644 index 0000000..e95c0e7 Binary files /dev/null and b/static/favicon-32x32.png differ diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..e7bcbcf Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/markdown.css b/static/markdown.css new file mode 100644 index 0000000..14be155 --- /dev/null +++ b/static/markdown.css @@ -0,0 +1,709 @@ +@font-face { + font-family: octicons-link; + src: url(data:font/woff;charset=utf-8;base64,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) format('woff'); +} + +.markdown-body { + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + line-height: 1.5; + color: #24292e; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + font-size: 16px; + line-height: 1.5; + word-wrap: break-word; +} + +.markdown-body { + box-sizing: border-box; + min-width: 200px; + max-width: 980px; + margin: 0 auto; + padding: 45px; +} + +@media (max-width: 767px) { + .markdown-body { + padding: 15px; + } +} + +.markdown-body .pl-c { + color: #6a737d; +} + +.markdown-body .pl-c1, +.markdown-body .pl-s .pl-v { + color: #005cc5; +} + +.markdown-body .pl-e, +.markdown-body .pl-en { + color: #6f42c1; +} + +.markdown-body .pl-smi, +.markdown-body .pl-s .pl-s1 { + color: #24292e; +} + +.markdown-body .pl-ent { + color: #22863a; +} + +.markdown-body .pl-k { + color: #d73a49; +} + +.markdown-body .pl-s, +.markdown-body .pl-pds, +.markdown-body .pl-s .pl-pse .pl-s1, +.markdown-body .pl-sr, +.markdown-body .pl-sr .pl-cce, +.markdown-body .pl-sr .pl-sre, +.markdown-body .pl-sr .pl-sra { + color: #032f62; +} + +.markdown-body .pl-v, +.markdown-body .pl-smw { + color: #e36209; +} + +.markdown-body .pl-bu { + color: #b31d28; +} + +.markdown-body .pl-ii { + color: #fafbfc; + background-color: #b31d28; +} + +.markdown-body .pl-c2 { + color: #fafbfc; + background-color: #d73a49; +} + +.markdown-body .pl-c2::before { + content: "^M"; +} + +.markdown-body .pl-sr .pl-cce { + font-weight: bold; + color: #22863a; +} + +.markdown-body .pl-ml { + color: #735c0f; +} + +.markdown-body .pl-mh, +.markdown-body .pl-mh .pl-en, +.markdown-body .pl-ms { + font-weight: bold; + color: #005cc5; +} + +.markdown-body .pl-mi { + font-style: italic; + color: #24292e; +} + +.markdown-body .pl-mb { + font-weight: bold; + color: #24292e; +} + +.markdown-body .pl-md { + color: #b31d28; + background-color: #ffeef0; +} + +.markdown-body .pl-mi1 { + color: #22863a; + background-color: #f0fff4; +} + +.markdown-body .pl-mc { + color: #e36209; + background-color: #ffebda; +} + +.markdown-body .pl-mi2 { + color: #f6f8fa; + background-color: #005cc5; +} + +.markdown-body .pl-mdr { + font-weight: bold; + color: #6f42c1; +} + +.markdown-body .pl-ba { + color: #586069; +} + +.markdown-body .pl-sg { + color: #959da5; +} + +.markdown-body .pl-corl { + text-decoration: underline; + color: #032f62; +} + +.markdown-body .octicon { + display: inline-block; + vertical-align: text-top; + fill: currentColor; +} + +.markdown-body a { + background-color: transparent; +} + +.markdown-body a:active, +.markdown-body a:hover { + outline-width: 0; +} + +.markdown-body strong { + font-weight: inherit; +} + +.markdown-body strong { + font-weight: bolder; +} + +.markdown-body h1 { + font-size: 2em; + margin: 0.67em 0; +} + +.markdown-body img { + border-style: none; +} + +.markdown-body code, +.markdown-body kbd, +.markdown-body pre { + font-family: monospace, monospace; + font-size: 1em; +} + +.markdown-body hr { + box-sizing: content-box; + height: 0; + overflow: visible; +} + +.markdown-body input { + font: inherit; + margin: 0; +} + +.markdown-body input { + overflow: visible; +} + +.markdown-body [type="checkbox"] { + box-sizing: border-box; + padding: 0; +} + +.markdown-body * { + box-sizing: border-box; +} + +.markdown-body input { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +.markdown-body a { + color: #0366d6; + text-decoration: none; +} + +.markdown-body a:hover { + text-decoration: underline; +} + +.markdown-body strong { + font-weight: 600; +} + +.markdown-body hr { + height: 0; + margin: 15px 0; + overflow: hidden; + background: transparent; + border: 0; + border-bottom: 1px solid #dfe2e5; +} + +.markdown-body hr::before { + display: table; + content: ""; +} + +.markdown-body hr::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body table { + border-spacing: 0; + border-collapse: collapse; +} + +.markdown-body td, +.markdown-body th { + padding: 0; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body h1 { + font-size: 32px; + font-weight: 600; +} + +.markdown-body h2 { + font-size: 24px; + font-weight: 600; +} + +.markdown-body h3 { + font-size: 20px; + font-weight: 600; +} + +.markdown-body h4 { + font-size: 16px; + font-weight: 600; +} + +.markdown-body h5 { + font-size: 14px; + font-weight: 600; +} + +.markdown-body h6 { + font-size: 12px; + font-weight: 600; +} + +.markdown-body p { + margin-top: 0; + margin-bottom: 10px; +} + +.markdown-body blockquote { + margin: 0; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 0; + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body ol ol, +.markdown-body ul ol { + list-style-type: lower-roman; +} + +.markdown-body ul ul ol, +.markdown-body ul ol ol, +.markdown-body ol ul ol, +.markdown-body ol ol ol { + list-style-type: lower-alpha; +} + +.markdown-body dd { + margin-left: 0; +} + +.markdown-body code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body pre { + margin-top: 0; + margin-bottom: 0; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; +} + +.markdown-body .octicon { + vertical-align: text-bottom; +} + +.markdown-body .pl-0 { + padding-left: 0 !important; +} + +.markdown-body .pl-1 { + padding-left: 4px !important; +} + +.markdown-body .pl-2 { + padding-left: 8px !important; +} + +.markdown-body .pl-3 { + padding-left: 16px !important; +} + +.markdown-body .pl-4 { + padding-left: 24px !important; +} + +.markdown-body .pl-5 { + padding-left: 32px !important; +} + +.markdown-body .pl-6 { + padding-left: 40px !important; +} + +.markdown-body::before { + display: table; + content: ""; +} + +.markdown-body::after { + display: table; + clear: both; + content: ""; +} + +.markdown-body>*:first-child { + margin-top: 0 !important; +} + +.markdown-body>*:last-child { + margin-bottom: 0 !important; +} + +.markdown-body a:not([href]) { + color: inherit; + text-decoration: none; +} + +.markdown-body .anchor { + float: left; + padding-right: 4px; + margin-left: -20px; + line-height: 1; +} + +.markdown-body .anchor:focus { + outline: none; +} + +.markdown-body p, +.markdown-body blockquote, +.markdown-body ul, +.markdown-body ol, +.markdown-body dl, +.markdown-body table, +.markdown-body pre { + margin-top: 0; + margin-bottom: 16px; +} + +.markdown-body hr { + height: 0.25em; + padding: 0; + margin: 24px 0; + background-color: #e1e4e8; + border: 0; +} + +.markdown-body blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #dfe2e5; +} + +.markdown-body blockquote>:first-child { + margin-top: 0; +} + +.markdown-body blockquote>:last-child { + margin-bottom: 0; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4, +.markdown-body h5, +.markdown-body h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; +} + +.markdown-body h1 .octicon-link, +.markdown-body h2 .octicon-link, +.markdown-body h3 .octicon-link, +.markdown-body h4 .octicon-link, +.markdown-body h5 .octicon-link, +.markdown-body h6 .octicon-link { + color: #1b1f23; + vertical-align: middle; + visibility: hidden; +} + +.markdown-body h1:hover .anchor, +.markdown-body h2:hover .anchor, +.markdown-body h3:hover .anchor, +.markdown-body h4:hover .anchor, +.markdown-body h5:hover .anchor, +.markdown-body h6:hover .anchor { + text-decoration: none; +} + +.markdown-body h1:hover .anchor .octicon-link, +.markdown-body h2:hover .anchor .octicon-link, +.markdown-body h3:hover .anchor .octicon-link, +.markdown-body h4:hover .anchor .octicon-link, +.markdown-body h5:hover .anchor .octicon-link, +.markdown-body h6:hover .anchor .octicon-link { + visibility: visible; +} + +.markdown-body h1 { + padding-bottom: 0.3em; + font-size: 2em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h2 { + padding-bottom: 0.3em; + font-size: 1.5em; + border-bottom: 1px solid #eaecef; +} + +.markdown-body h3 { + font-size: 1.25em; +} + +.markdown-body h4 { + font-size: 1em; +} + +.markdown-body h5 { + font-size: 0.875em; +} + +.markdown-body h6 { + font-size: 0.85em; + color: #6a737d; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 2em; +} + +.markdown-body ul ul, +.markdown-body ul ol, +.markdown-body ol ol, +.markdown-body ol ul { + margin-top: 0; + margin-bottom: 0; +} + +.markdown-body li { + word-wrap: break-all; +} + +.markdown-body li>p { + margin-top: 16px; +} + +.markdown-body li+li { + margin-top: 0.25em; +} + +.markdown-body dl { + padding: 0; +} + +.markdown-body dl dt { + padding: 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: 600; +} + +.markdown-body dl dd { + padding: 0 16px; + margin-bottom: 16px; +} + +.markdown-body table { + display: block; + width: 100%; + overflow: auto; +} + +.markdown-body table th { + font-weight: 600; +} + +.markdown-body table th, +.markdown-body table td { + padding: 6px 13px; + border: 1px solid #dfe2e5; +} + +.markdown-body table tr { + background-color: #fff; + border-top: 1px solid #c6cbd1; +} + +.markdown-body table tr:nth-child(2n) { + background-color: #f6f8fa; +} + +.markdown-body img { + max-width: 100%; + box-sizing: content-box; + background-color: #fff; +} + +.markdown-body img[align=right] { + padding-left: 20px; +} + +.markdown-body img[align=left] { + padding-right: 20px; +} + +.markdown-body code { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; +} + +.markdown-body pre { + word-wrap: normal; +} + +.markdown-body pre>code { + padding: 0; + margin: 0; + font-size: 100%; + word-break: normal; + white-space: pre; + background: transparent; + border: 0; +} + +.markdown-body .highlight { + margin-bottom: 16px; +} + +.markdown-body .highlight pre { + margin-bottom: 0; + word-break: normal; +} + +.markdown-body .highlight pre, +.markdown-body pre { + padding: 16px; + overflow: auto; + font-size: 85%; + line-height: 1.45; + background-color: #f6f8fa; + border-radius: 3px; +} + +.markdown-body pre code { + display: inline; + max-width: auto; + padding: 0; + margin: 0; + overflow: visible; + line-height: inherit; + word-wrap: normal; + background-color: transparent; + border: 0; +} + +.markdown-body .full-commit .btn-outline:not(:disabled):hover { + color: #005cc5; + border-color: #005cc5; +} + +.markdown-body kbd { + display: inline-block; + padding: 3px 5px; + font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #444d56; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #d1d5da; + border-bottom-color: #c6cbd1; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #c6cbd1; +} + +.markdown-body :checked+.radio-label { + position: relative; + z-index: 1; + border-color: #0366d6; +} + +.markdown-body .task-list-item { + list-style-type: none; +} + +.markdown-body .task-list-item+.task-list-item { + margin-top: 3px; +} + +.markdown-body .task-list-item input { + margin: 0 0.2em 0.25em -1.6em; + vertical-align: middle; +} + +.markdown-body hr { + border-bottom-color: #eee; +} diff --git a/static/markdown.min.css b/static/markdown.min.css new file mode 100644 index 0000000..867bffa --- /dev/null +++ b/static/markdown.min.css @@ -0,0 +1 @@ +@font-face{font-family:octicons-link;src:url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff')}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#24292e;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:16px;line-height:1.5;word-wrap:break-word;box-sizing:border-box;min-width:200px;max-width:980px;margin:0 auto;padding:45px}@media (max-width:767px){.markdown-body{padding:15px}}.markdown-body .pl-c{color:#6a737d}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#005cc5}.markdown-body .pl-e,.markdown-body .pl-en{color:#6f42c1}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#24292e}.markdown-body .pl-ent{color:#22863a}.markdown-body .pl-k{color:#d73a49}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#032f62}.markdown-body .pl-smw,.markdown-body .pl-v{color:#e36209}.markdown-body .pl-bu{color:#b31d28}.markdown-body .pl-ii{color:#fafbfc;background-color:#b31d28}.markdown-body .pl-c2{color:#fafbfc;background-color:#d73a49}.markdown-body .pl-c2::before{content:"^M"}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#22863a}.markdown-body .pl-ml{color:#735c0f}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#005cc5}.markdown-body .pl-mi{font-style:italic;color:#24292e}.markdown-body .pl-mb{font-weight:700;color:#24292e}.markdown-body .pl-md{color:#b31d28;background-color:#ffeef0}.markdown-body .pl-mi1{color:#22863a;background-color:#f0fff4}.markdown-body .pl-mc{color:#e36209;background-color:#ffebda}.markdown-body .pl-mi2{color:#f6f8fa;background-color:#005cc5}.markdown-body .pl-mdr{font-weight:700;color:#6f42c1}.markdown-body .pl-ba{color:#586069}.markdown-body .pl-sg{color:#959da5}.markdown-body .pl-corl{text-decoration:underline;color:#032f62}.markdown-body .octicon{display:inline-block;fill:currentColor}.markdown-body a{background-color:transparent;color:#0366d6;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:600}.markdown-body h1{margin:.67em 0}.markdown-body img{border-style:none;max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body hr{box-sizing:content-box;overflow:hidden;background:0 0;height:.25em;padding:0;margin:24px 0;background-color:#e1e4e8;border:0;border-bottom-color:#eee}.markdown-body input{font:inherit;margin:0;overflow:visible;font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body a:hover{text-decoration:underline}.markdown-body hr::before,.markdown-body::before{display:table;content:""}.markdown-body hr::after,.markdown-body::after{display:table;clear:both;content:""}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body blockquote{margin:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{padding:.2em .4em;margin:0;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body code,.markdown-body pre{font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:4px!important}.markdown-body .pl-2{padding-left:8px!important}.markdown-body .pl-3{padding-left:16px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:32px!important}.markdown-body .pl-6{padding-left:40px!important}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:0}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace;line-height:10px;color:#444d56;vertical-align:middle;background-color:#fafbfc;border:solid 1px #d1d5da;border-bottom-color:#c6cbd1;border-radius:3px;box-shadow:inset 0 -1px 0 #c6cbd1}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#1b1f23;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;font-size:2em;border-bottom:1px solid #eaecef}.markdown-body h2{font-size:1.5em}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#6a737d}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li{word-wrap:break-all}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:600}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:600}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #dfe2e5}.markdown-body table tr{background-color:#fff;border-top:1px solid #c6cbd1}.markdown-body table tr:nth-child(2n){background-color:#f6f8fa}.markdown-body img[align=right]{padding-left:20px}.markdown-body img[align=left]{padding-right:20px}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:0 0;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f6f8fa;border-radius:3px}.markdown-body pre code{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#005cc5;border-color:#005cc5}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#0366d6}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle} \ No newline at end of file diff --git a/static/safari-pinned-tab.svg b/static/safari-pinned-tab.svg new file mode 100644 index 0000000..44c0669 --- /dev/null +++ b/static/safari-pinned-tab.svg @@ -0,0 +1,61 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/templates/code.html.tera b/templates/code.html.tera new file mode 100644 index 0000000..c81f633 --- /dev/null +++ b/templates/code.html.tera @@ -0,0 +1,41 @@ + + + + + Source Code | Rocket Powered Pastebin + + + + + + + + + + + +
+ + + + + +
+
+              {%- for l in range(start=1,end=lines) -%}
+              {{ l }}

+              {%- endfor -%}
+            
+
+ {{ content | safe }} +
+
+ + diff --git a/templates/index.html.tera b/templates/index.html.tera new file mode 100644 index 0000000..3f92b4f --- /dev/null +++ b/templates/index.html.tera @@ -0,0 +1,102 @@ + + + + + Rocket Powered Pastebin + + + + + + + +
+  ABOUT
+
+      A simple pastebin powered by Rocket.
+
+      Simple API. CLI. Web form. Renders Markdown. Highlights code.
+
+      Web Form: {{ config.server_url }}/web
+
+  API USAGE
+
+      POST {{ config.server_url }}/
+
+          Send the raw data along. Will respond with a link to the paste.
+
+          If the response code is 201 (CREATED), then the entire paste was
+          uploaded. If the response is 206 (PARTIAL), then the paste exceeded
+          the server's maximum upload size, and only part of the paste was
+          uploaded. If the response code is anything else, an error has
+          occurred. Pasting is heavily rate limited.
+
+      GET {{ config.server_url }}/<id>
+
+          Retrieve the paste with the given id as plain-text.
+
+      GET {{ config.server_url }}/<id>.<ext>
+
+          Retrieve the paste with the given id.
+
+          If ext is "md", "mdown", or "markdown", the paste is rendered as
+          markdown into HTML. If ext is a known code file extension, the paste
+          is syntax highlighted and returned as HTML. If ext is a known format
+          extension, the paste is returned with the format's corresponding
+          Content-Type. Otherwise, the paste is returned as unmodified text.
+
+      DELETE {{ config.server_url }}/<id>
+
+          Delete the paste with the given id.
+
+  EXAMPLES
+
+      Paste a file named 'file.txt' using {{ cmd }}:
+
+          {% if os == "windows" -%}
+              Invoke-RestMethod -Uri "{{ config.server_url }}" -Method Post -InFile .\file.txt
+          {%- else -%}
+              curl --data-binary @file.txt {{ config.server_url }}/
+          {%- endif %}
+
+      Paste from stdin using {{ cmd }}:
+
+          {% if os == "windows" -%}
+              echo "Hi!" | Invoke-RestMethod -Uri "{{ config.server_url }}" -Method Post
+          {%- else -%}
+              echo "Hello, world." | curl --data-binary @- {{ config.server_url }}/
+          {%- endif %}
+
+      Delete an existing paste with id <id> using {{ cmd }}:
+
+          {% if os == "windows" -%}
+              Invoke-RestMethod -Uri "{{ config.server_url }}/<id>" -Method Delete
+          {%- else -%}
+              curl -X DELETE {{ config.server_url }}/<id>
+          {%- endif %}
+
+      {% if os == "windows" -%}
+         A {{ cmd }} function that can be used for quick pasting from the
+      command line. The command takes a filename or reads from stdin if none was
+      supplied and outputs the URL of the paste to stdout: `Paste file.txt` or
+      `echo "hi" | Paste`.
+
+          function Paste([string]$file) {
+              $Data = if ($file) {Get-Content $file} else {$input}
+              Invoke-RestMethod -Uri "{{ config.server_url }}" -Method Post -Body $Data
+          }
+      {%- else -%}
+         A shell function that can be added to `.bashrc` or `.bash_profle` for
+      quick pasting from the command line. The command takes a filename or reads
+      from stdin if none was supplied and outputs the URL of the paste to
+      stdout: `paste file.txt` or `echo "hi" | paste`.
+
+          function paste() {
+              local file=${1:-/dev/stdin}
+              curl --data-binary @${file} {{ config.server_url }}
+          }
+      {%- endif %}
+  
+ diff --git a/templates/markdown.html.tera b/templates/markdown.html.tera new file mode 100644 index 0000000..516a09b --- /dev/null +++ b/templates/markdown.html.tera @@ -0,0 +1,27 @@ + + + + + Markdown | Rocket Powered Pastebin + + + + + + + + + + + +
{{ content | safe }}
+ + diff --git a/templates/new.html.tera b/templates/new.html.tera new file mode 100644 index 0000000..ffa5f87 --- /dev/null +++ b/templates/new.html.tera @@ -0,0 +1,62 @@ + + + + + New Paste | Rocket Powered Pastebin + + + + + + + + + +
+ {% if error %} +
+ Error: {{ error }} +
+ {% endif %} + + + +
+ + +
+ + +
+ + diff --git a/upload/.gitignore b/upload/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/upload/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore