├── .envrc ├── .gitignore ├── .pipelight_ignore ├── Cargo.lock ├── Cargo.toml ├── INTERNALS.md ├── LICENSE.md ├── README.md ├── cli ├── Cargo.toml └── src │ ├── lib.rs │ └── types.rs ├── examples └── jucenit.toml ├── flake.lock ├── flake.nix ├── jucenit.toml ├── jucenit ├── Cargo.toml └── src │ └── main.rs ├── jucenit_core ├── Cargo.toml └── src │ ├── cast │ ├── config.rs │ ├── from.rs │ ├── from_database.rs │ ├── mod.rs │ └── to_database │ │ ├── delete.rs │ │ ├── insert.rs │ │ ├── methods.rs │ │ └── mod.rs │ ├── database │ ├── crud.rs │ ├── entity │ │ ├── action.rs │ │ ├── host.rs │ │ ├── listener.rs │ │ ├── match_action.rs │ │ ├── match_host.rs │ │ ├── match_listener.rs │ │ ├── mod.rs │ │ ├── ng_match.rs │ │ └── prelude.rs │ └── mod.rs │ ├── error │ └── mod.rs │ ├── lib.rs │ ├── nginx │ ├── certificate │ │ ├── crud.rs │ │ ├── mappings.rs │ │ ├── methods.rs │ │ └── mod.rs │ ├── config │ │ ├── crud.rs │ │ ├── from.rs │ │ └── mod.rs │ ├── from_database.rs │ ├── mod.rs │ └── options.rs │ └── ssl │ ├── fake.rs │ ├── letsencrypt.rs │ ├── mod.rs │ └── pebble.rs ├── migration ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── m20240606_110915_create_table.rs │ └── main.rs ├── module.nix ├── package.nix ├── pipelight.ts ├── rust-toolchain.toml └── shell.nix /.envrc: -------------------------------------------------------------------------------- 1 | # Old way 2 | # Uses shell.nix 3 | # 4 | # use nix shell.darwin.nix 5 | # use nix 6 | 7 | # New way uses flakes 8 | use flake 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Pipelight internal api folder 2 | .pipelight 3 | 4 | # Test artifacts 5 | test/*pipelight* 6 | test_* 7 | 8 | ## Build directories 9 | /packages 10 | /autocompletion 11 | /target 12 | # Nixos 13 | /result 14 | gifs/ 15 | 16 | ## Nixos direnv cache 17 | .direnv 18 | 19 | -------------------------------------------------------------------------------- /.pipelight_ignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .pipelight/ 3 | 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "cli", # command line definition 5 | "jucenit", # binary entrypoint 6 | "jucenit_core", # requests to nginx API 7 | "migration", # generate database 8 | ] 9 | -------------------------------------------------------------------------------- /INTERNALS.md: -------------------------------------------------------------------------------- 1 | # Jucenit internals 2 | 3 | ## What it is. 4 | 5 | I do not have the time to maintain such a good piece of software 6 | as a mature server like nginx-unit. 7 | 8 | Jucenit is only a translation layer from an easy toml syntax 9 | to nginx-unit configuration file. 10 | 11 | Plus some utilities to ease ssl renewal and live edit configuration. 12 | 13 | ## Crate structure 14 | 15 | Database crates: 16 | 17 | - migration: Database schema definition. 18 | - entity: Autogenerated with ORM. 19 | 20 | Convenience crates: 21 | 22 | - jucenit: Entrypoint for binary. 23 | - utils: Pipelight utility crate for filesystem and process management. 24 | 25 | Core crate: 26 | 27 | - jucenit_core: The main crate were the everything is done. 28 | Core modules: 29 | - cast: toml to database entities 30 | - nginx: database entities to json and ssl renewal. 31 | 32 | ## How it works. 33 | 34 | ### File type convertion. 35 | 36 | The toml configuration is split into small related entities that are pushed to a relational database (sqlite). 37 | The entities are then taken from the database, 38 | and reassembled into the equivalent nginx-unit configuration. 39 | 40 | Yes, the database serves the solo purpose of file convertion, 41 | so it is has a pretty simple schema, but it drastically decreases the code comlexity. 42 | 43 | ```mermaid 44 | graph LR 45 | 46 | A(jucenit.toml) ---> B{Database} ---> C(nginx-unit.json) 47 | 48 | ``` 49 | 50 | The database schema is defined inside the **migration crate** through a practical rust orm 51 | [sea_orm](https://www.sea-ql.org/SeaORM/docs/index/). 52 | 53 | _I have been traumatised with ORM so I could have written raw SQL, but SeaORM really 54 | does a pleasant heavy lifting._ 55 | 56 | Simplified diagram without relation tables. 57 | 58 | ```mermaid 59 | classDiagram 60 | Match <|-- Action 61 | Match <|-- Host 62 | Match <|-- Listener 63 | 64 | class Match { 65 | raw_parameters 66 | } 67 | class Host { 68 | raw_parameters 69 | } 70 | class Listener { 71 | raw_parameters 72 | } 73 | class Action { 74 | raw_parameters 75 | } 76 | 77 | ``` 78 | 79 | Complete diagram with relation tables. 80 | 81 | Every relations are many to many through a relation table. 82 | 83 | ```mermaid 84 | classDiagram 85 | Match <|-- MatchListener 86 | MatchListener <|-- Listener 87 | 88 | Match <|-- MatchHost 89 | MatchHost <|-- Host 90 | 91 | Match <|-- Action 92 | 93 | class Host { 94 | +int id 95 | +String domain 96 | } 97 | class Match { 98 | +int id 99 | +int uuid 100 | +int action_id 101 | +String raw_params 102 | } 103 | class Listener { 104 | +int id 105 | +String raw_params 106 | } 107 | class Action { 108 | +int id 109 | +String raw_params 110 | } 111 | class MatchHost { 112 | +int id_match 113 | +int id_host 114 | } 115 | class MatchListener { 116 | +int id_match 117 | +int id_listener 118 | } 119 | ``` 120 | 121 | The column `raw_params` are the nginx-unit arguments stored as json. 122 | 123 | Because nginx-unit and jucenit will evolve for the better, and for the sake of simplicity, 124 | their is no strong mapping between jucenit and nginx through clearly defined rust Structs(type definitions). 125 | Consequences are that jucenit will always accept arguments that are accepted by nginx-unit 126 | without the need to update jucenit internals Structs. 127 | 128 | ### Auto Ssl (tls certificate management) 129 | 130 | Relate on a slighty modified version of [acme2](https://docs.rs/acme2/latest/acme2/) crate, 131 | which is [pipelight-acme2](https://github.com/pipelight/acme2). 132 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | 125 | Thus, it is not the intent of this section to claim rights or contest 126 | your rights to work written entirely by you; rather, the intent is to 127 | exercise the right to control the distribution of derivative or 128 | collective works based on the Program. 129 | 130 | In addition, mere aggregation of another work not based on the Program 131 | with the Program (or with a work based on the Program) on a volume of 132 | a storage or distribution medium does not bring the other work under 133 | the scope of this License. 134 | 135 | 3. You may copy and distribute the Program (or a work based on it, 136 | under Section 2) in object code or executable form under the terms of 137 | Sections 1 and 2 above provided that you also do one of the following: 138 | 139 | a) Accompany it with the complete corresponding machine-readable 140 | source code, which must be distributed under the terms of Sections 141 | 1 and 2 above on a medium customarily used for software interchange; or, 142 | 143 | b) Accompany it with a written offer, valid for at least three 144 | years, to give any third party, for a charge no more than your 145 | cost of physically performing source distribution, a complete 146 | machine-readable copy of the corresponding source code, to be 147 | distributed under the terms of Sections 1 and 2 above on a medium 148 | customarily used for software interchange; or, 149 | 150 | c) Accompany it with the information you received as to the offer 151 | to distribute corresponding source code. (This alternative is 152 | allowed only for noncommercial distribution and only if you 153 | received the program in object code or executable form with such 154 | an offer, in accord with Subsection b above.) 155 | 156 | The source code for a work means the preferred form of the work for 157 | making modifications to it. For an executable work, complete source 158 | code means all the source code for all modules it contains, plus any 159 | associated interface definition files, plus the scripts used to 160 | control compilation and installation of the executable. However, as a 161 | special exception, the source code distributed need not include 162 | anything that is normally distributed (in either source or binary 163 | form) with the major components (compiler, kernel, and so on) of the 164 | operating system on which the executable runs, unless that component 165 | itself accompanies the executable. 166 | 167 | If distribution of executable or object code is made by offering 168 | access to copy from a designated place, then offering equivalent 169 | access to copy the source code from the same place counts as 170 | distribution of the source code, even though third parties are not 171 | compelled to copy the source along with the object code. 172 | 173 | 4. You may not copy, modify, sublicense, or distribute the Program 174 | except as expressly provided under this License. Any attempt 175 | otherwise to copy, modify, sublicense or distribute the Program is 176 | void, and will automatically terminate your rights under this License. 177 | However, parties who have received copies, or rights, from you under 178 | this License will not have their licenses terminated so long as such 179 | parties remain in full compliance. 180 | 181 | 5. You are not required to accept this License, since you have not 182 | signed it. However, nothing else grants you permission to modify or 183 | distribute the Program or its derivative works. These actions are 184 | prohibited by law if you do not accept this License. Therefore, by 185 | modifying or distributing the Program (or any work based on the 186 | Program), you indicate your acceptance of this License to do so, and 187 | all its terms and conditions for copying, distributing or modifying 188 | the Program or works based on it. 189 | 190 | 6. Each time you redistribute the Program (or any work based on the 191 | Program), the recipient automatically receives a license from the 192 | original licensor to copy, distribute or modify the Program subject to 193 | these terms and conditions. You may not impose any further 194 | restrictions on the recipients' exercise of the rights granted herein. 195 | You are not responsible for enforcing compliance by third parties to 196 | this License. 197 | 198 | 7. If, as a consequence of a court judgment or allegation of patent 199 | infringement or for any other reason (not limited to patent issues), 200 | conditions are imposed on you (whether by court order, agreement or 201 | otherwise) that contradict the conditions of this License, they do not 202 | excuse you from the conditions of this License. If you cannot 203 | distribute so as to satisfy simultaneously your obligations under this 204 | License and any other pertinent obligations, then as a consequence you 205 | may not distribute the Program at all. For example, if a patent 206 | license would not permit royalty-free redistribution of the Program by 207 | all those who receive copies directly or indirectly through you, then 208 | the only way you could satisfy both it and this License would be to 209 | refrain entirely from distribution of the Program. 210 | 211 | If any portion of this section is held invalid or unenforceable under 212 | any particular circumstance, the balance of the section is intended to 213 | apply and the section as a whole is intended to apply in other 214 | circumstances. 215 | 216 | It is not the purpose of this section to induce you to infringe any 217 | patents or other property right claims or to contest validity of any 218 | such claims; this section has the sole purpose of protecting the 219 | integrity of the free software distribution system, which is 220 | implemented by public license practices. Many people have made 221 | generous contributions to the wide range of software distributed 222 | through that system in reliance on consistent application of that 223 | system; it is up to the author/donor to decide if he or she is willing 224 | to distribute software through any other system and a licensee cannot 225 | impose that choice. 226 | 227 | This section is intended to make thoroughly clear what is believed to 228 | be a consequence of the rest of this License. 229 | 230 | 8. If the distribution and/or use of the Program is restricted in 231 | certain countries either by patents or by copyrighted interfaces, the 232 | original copyright holder who places the Program under this License 233 | may add an explicit geographical distribution limitation excluding 234 | those countries, so that distribution is permitted only in or among 235 | countries not thus excluded. In such case, this License incorporates 236 | the limitation as if written in the body of this License. 237 | 238 | 9. The Free Software Foundation may publish revised and/or new versions 239 | of the General Public License from time to time. Such new versions will 240 | be similar in spirit to the present version, but may differ in detail to 241 | address new problems or concerns. 242 | 243 | Each version is given a distinguishing version number. If the Program 244 | specifies a version number of this License which applies to it and "any 245 | later version", you have the option of following the terms and conditions 246 | either of that version or of any later version published by the Free 247 | Software Foundation. If the Program does not specify a version number of 248 | this License, you may choose any version ever published by the Free Software 249 | Foundation. 250 | 251 | 10. If you wish to incorporate parts of the Program into other free 252 | programs whose distribution conditions are different, write to the author 253 | to ask for permission. For software which is copyrighted by the Free 254 | Software Foundation, write to the Free Software Foundation; we sometimes 255 | make exceptions for this. Our decision will be guided by the two goals 256 | of preserving the free status of all derivatives of our free software and 257 | of promoting the sharing and reuse of software generally. 258 | 259 | NO WARRANTY 260 | 261 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 262 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 263 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 264 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 265 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 266 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 267 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 268 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 269 | REPAIR OR CORRECTION. 270 | 271 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 272 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 273 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 274 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 275 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 276 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 277 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 278 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 279 | POSSIBILITY OF SUCH DAMAGES. 280 | 281 | END OF TERMS AND CONDITIONS 282 | 283 | How to Apply These Terms to Your New Programs 284 | 285 | If you develop a new program, and you want it to be of the greatest 286 | possible use to the public, the best way to achieve this is to make it 287 | free software which everyone can redistribute and change under these terms. 288 | 289 | To do so, attach the following notices to the program. It is safest 290 | to attach them to the start of each source file to most effectively 291 | convey the exclusion of warranty; and each file should have at least 292 | the "copyright" line and a pointer to where the full notice is found. 293 | 294 | 295 | Copyright (C) 296 | 297 | This program is free software; you can redistribute it and/or modify 298 | it under the terms of the GNU General Public License as published by 299 | the Free Software Foundation; either version 2 of the License, or 300 | (at your option) any later version. 301 | 302 | This program is distributed in the hope that it will be useful, 303 | but WITHOUT ANY WARRANTY; without even the implied warranty of 304 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 305 | GNU General Public License for more details. 306 | 307 | You should have received a copy of the GNU General Public License along 308 | with this program; if not, write to the Free Software Foundation, Inc., 309 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Lesser General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jucenit - A simple web server. 2 | 3 | Warning: 4 | 5 | **Early development stage.** 6 | Do not use at home. 7 | You might not want to come back to other web servers. 8 | 9 | The API is still undergoing some small changes. 10 | 11 | Jucenit is a web server configurable through short scattered toml files. 12 | Internally uses [nginx unit](https://github.com/nginx/unit). 13 | 14 | ## Features 15 | 16 | - **Split** your configuration across multiple files in **Toml**. 17 | - **Easy ssl** renewal. 18 | 19 | ## Usage 20 | 21 | ### Expose services 22 | 23 | **Your configuration chunks must be uniquely identified with a mandatory uuid.** 24 | 25 | Use it as a reverse-proxy. 26 | 27 | ```toml 28 | # jucenit.toml 29 | [[unit]] 30 | uuid = "d3630938-5851-43ab-a523-84e0c6af9eb1" 31 | listeners = ["*:443"] 32 | [unit.match] 33 | hosts = ["example.com"] 34 | [unit.action] 35 | proxy = "http://127.0.0.1:8888" 36 | ``` 37 | 38 | On queries like "https://example.com" 39 | it redirects to the port 8888 on private network. 40 | 41 | Or for file sharing 42 | 43 | ```toml 44 | # jucenit.toml 45 | [[unit]] 46 | uuid = "f37490cb-d4eb-4f37-bb85-d39dad6a21ab" 47 | listeners = ["*:443"] 48 | [unit.match] 49 | hosts = ["test.com"] 50 | uri = "/static" 51 | [unit.action] 52 | share = ["/home/website/static"] 53 | ``` 54 | 55 | On queries like "https://test.com/static/index.html" 56 | it redirects to /home/website/static/index.html 57 | 58 | And many more possibilities at [nginx unit](https://github.com/nginx/unit). 59 | Update the global configuration with your configuration chunks. 60 | 61 | ```sh 62 | jucenit push 63 | # or 64 | jucenit push --file jucenit.toml 65 | ``` 66 | 67 | ### Edit the global configuration 68 | 69 | The only way to cherry remove chunks from the global configuration 70 | is to edit the main configuration with: 71 | 72 | ```sh 73 | jucenit edit 74 | ``` 75 | 76 | Or to delete everything previously pushed to the global configuration 77 | 78 | ```sh 79 | jucenit clean 80 | ``` 81 | 82 | ### Tls/Ssl management 83 | 84 | Add new certificates or Renew almost expired certificates. 85 | 86 | ```sh 87 | jucenit ssl --renew 88 | ``` 89 | 90 | Remove every certificates. 91 | 92 | ```sh 93 | jucenit ssl --clean 94 | ``` 95 | 96 | Run the daemon for automatic certificate creation and renewal 97 | 98 | ```sh 99 | jucenit ssl --watch 100 | ``` 101 | 102 | ## How it works ? 103 | 104 | See detailed project structure and functionning at [INTERNALS.md](https://github.com/pipelight/jucenit/INTERNALS.md) 105 | 106 | ## Install 107 | 108 | ### with Nix and Nixos 109 | 110 | First, add the flake url to your flakes **inputs**. 111 | 112 | ```nix 113 | inputs = { 114 | jucenit.url = "github:pipelight/jucenit"; 115 | }; 116 | ``` 117 | 118 | And enable the service in your configuration file; 119 | 120 | ```nix 121 | services.jucenit.enable = true; 122 | ``` 123 | 124 | ### with Cargo 125 | 126 | You first need a running instance of nginx-unit. 127 | See the [installation guide](https://unit.nginx.org/installation/): 128 | 129 | Add the following configuration changes: 130 | 131 | ```sh 132 | unitd --control '127.0.0.1:8080' 133 | ``` 134 | 135 | So it listens on tcp port 8080 instead of default unix socket. 136 | 137 | Install on any linux distribution with cargo. 138 | 139 | ```sh 140 | cargo install --git https://github.com/pipelight/jucenit 141 | ``` 142 | 143 | You need to run a background deamon for autossl. 144 | 145 | Create a file like a systemd-unit file or an initd file 146 | for autossl. 147 | 148 | It must run the following command: 149 | 150 | ```sh 151 | jucenit ssl --watch 152 | ``` 153 | 154 | ## Roadmap 155 | 156 | cli: 157 | 158 | - [x] add command to edit global configuration with favorite editor. 159 | - [x] add option to allow passing a toml string instead of a config file path to the executable. 160 | - [ ] add "push -d" to remove a chunk from global configuration. 161 | 162 | ssl certificates: 163 | 164 | - [x] parallel certificate renewal 165 | - [x] provide a template systemd unit (with nginx-unit sandboxing of course) 166 | - [x] add support for acme challenge http-01 167 | - [ ] add support for acme challenge tls-ALPN-01 168 | 169 | automation: 170 | 171 | - [x] make a daemon that watches certificates validity 172 | 173 | global improvements: 174 | 175 | - [ ] add a verbosity flag and better tracing 176 | 177 | ## Authors note 178 | 179 | _We need better tooling to easily share our makings to the world._ 180 | 181 | Licensed under GNU GPLv2 Copyright (C) 2023 Areskul 182 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = "4.5.4" 10 | clap-verbosity = "2.1.0" 11 | miette = "7.2.0" 12 | serde = "1.0.200" 13 | serde_toml = "0.0.1" 14 | jucenit_core = { path = "../jucenit_core" } 15 | assert_cmd = "2.0.14" 16 | futures = "0.3.30" 17 | tokio = { version = "1.38.0", features = ["full"] } 18 | -------------------------------------------------------------------------------- /cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | pub use types::Cli; 3 | -------------------------------------------------------------------------------- /cli/src/types.rs: -------------------------------------------------------------------------------- 1 | // Clap - command line lib 2 | use clap::FromArgMatches; 3 | use clap::{builder::PossibleValue, Args, Command, Parser, Subcommand, ValueHint}; 4 | // Verbosity 5 | pub use clap_verbosity::Verbosity; 6 | use jucenit_core::nginx::CertificateStore; 7 | // Serde 8 | use serde::{Deserialize, Serialize}; 9 | // Error Handling 10 | use miette::Result; 11 | // 12 | use jucenit_core::{ConfigFile, NginxConfig}; 13 | 14 | /* 15 | The Cli struct is the entrypoint for command line argument parsing: 16 | It casts arguments into the appropriate struct. 17 | 18 | let args = Cli::from_arg_matches(&matches) 19 | 20 | */ 21 | #[derive(Debug, Clone, Parser)] 22 | pub struct Cli { 23 | /** 24 | The set of subcommands. 25 | */ 26 | #[command(subcommand)] 27 | pub commands: Commands, 28 | 29 | /** 30 | * The folowing args are global arguments available 31 | * for every subcommands. 32 | */ 33 | /// Set a config file 34 | // #[arg(long, global = true, hide = true, value_name="FILE" ,value_hint = ValueHint::FilePath)] 35 | // pub config: Option, 36 | 37 | /// Set verbosity level 38 | #[clap(flatten)] 39 | pub verbose: Verbosity, 40 | } 41 | impl Cli { 42 | pub fn hydrate() -> Result<()> { 43 | let cli = Cli::parse(); 44 | Ok(()) 45 | } 46 | pub async fn run() -> Result<()> { 47 | let cli = Cli::parse(); 48 | match cli.commands { 49 | Commands::Push(args) => { 50 | if let Some(file) = args.file { 51 | let config = ConfigFile::load(&file)?; 52 | config.push().await?; 53 | } else if let Some(raw) = args.raw { 54 | let config = ConfigFile::from_toml_str(&raw)?; 55 | config.push().await?; 56 | } else { 57 | let config = ConfigFile::get()?; 58 | config.push().await?; 59 | } 60 | } 61 | Commands::Clean => { 62 | let config = ConfigFile::default(); 63 | config.set().await?; 64 | } 65 | Commands::Edit => { 66 | let config = ConfigFile::pull().await?; 67 | config.edit().await?; 68 | } 69 | Commands::Ssl(args) => { 70 | if args.renew { 71 | CertificateStore::hydrate().await?; 72 | } 73 | if args.clean { 74 | CertificateStore::clean().await?; 75 | } 76 | if args.watch { 77 | CertificateStore::watch().await?; 78 | } 79 | } 80 | _ => { 81 | // Err 82 | } 83 | }; 84 | Ok(()) 85 | } 86 | } 87 | 88 | /* 89 | An enumaration over the differen types of commands available: 90 | */ 91 | #[derive(Debug, Clone, Eq, PartialEq, Subcommand)] 92 | pub enum Commands { 93 | #[command(arg_required_else_help = true)] 94 | Push(File), 95 | #[command(arg_required_else_help = true)] 96 | Ssl(Ssl), 97 | // Developper commands 98 | #[command(hide = true)] 99 | Clean, 100 | Edit, 101 | } 102 | 103 | #[derive(Debug, Clone, Eq, PartialEq, Parser)] 104 | pub struct File { 105 | #[arg(help = "A configuration file path, example: ./jucenit.toml", value_hint = ValueHint::FilePath)] 106 | pub file: Option, 107 | #[arg(help = "A toml/yaml string", long)] 108 | pub raw: Option, 109 | } 110 | 111 | #[derive(Debug, Clone, Eq, PartialEq, Parser)] 112 | pub struct Ssl { 113 | #[arg(long)] 114 | pub renew: bool, 115 | #[arg(long)] 116 | pub watch: bool, 117 | #[arg(long, hide = false)] 118 | pub clean: bool, 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::{Cli, Commands}; 124 | use clap::FromArgMatches; 125 | use clap::Parser; 126 | 127 | use assert_cmd::prelude::*; // Add methods on commands 128 | use miette::{IntoDiagnostic, Result}; 129 | use std::path::PathBuf; 130 | use std::process::Command; // Run commnds 131 | 132 | use jucenit_core::{CertificateStore, ConfigFile}; 133 | 134 | /** 135 | * Set a fresh testing environment: 136 | * - clean certificate store 137 | * - set minimal nginx configuration 138 | */ 139 | async fn set_testing_config() -> Result<()> { 140 | CertificateStore::clean().await?; 141 | 142 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 143 | path.push("../examples/jucenit.toml"); 144 | 145 | let config = ConfigFile::load(path.to_str().unwrap())?; 146 | config.set().await?; 147 | 148 | Ok(()) 149 | } 150 | 151 | fn parse_command_line() -> Result<()> { 152 | let e = "jucenit --help"; 153 | let os_str: Vec<&str> = e.split(' ').collect(); 154 | let cli = Cli::parse_from(os_str); 155 | println!("{:#?}", cli); 156 | Ok(()) 157 | } 158 | 159 | #[tokio::test] 160 | async fn push_config_file() -> Result<()> { 161 | set_testing_config().await?; 162 | let mut cmd = Command::cargo_bin("jucenit").into_diagnostic()?; 163 | cmd.arg("push").arg("../examples/jucenit.toml"); 164 | cmd.assert().success(); 165 | Ok(()) 166 | } 167 | 168 | // #[tokio::test] 169 | async fn renew_ssl() -> Result<()> { 170 | set_testing_config().await?; 171 | let mut cmd = Command::cargo_bin("jucenit").into_diagnostic()?; 172 | cmd.arg("ssl").arg("--renew"); 173 | cmd.assert().success(); 174 | Ok(()) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /examples/jucenit.toml: -------------------------------------------------------------------------------- 1 | # A merge of config files: example.com and test.com 2 | [[unit]] 3 | uuid = 'd3630938-5851-43ab-a523-84e0c6af9eb1' 4 | listeners = ['*:443'] 5 | [unit.match] 6 | hosts = ['test.com', 'example.com'] 7 | [unit.action] 8 | proxy = 'http://127.0.0.1:8333' 9 | 10 | [[unit]] 11 | uuid = 'd462482d-21f7-48d6-8360-528f9e664c2f' 12 | listeners = ['*:443'] 13 | [unit.match] 14 | uri = ['/home'] 15 | [unit.action] 16 | proxy = 'http://127.0.0.1:8333' 17 | 18 | [[unit]] 19 | uuid = 'cc4e626a-9354-480e-a78b-f9f845148984' 20 | listeners = ['*:443'] 21 | [unit.match] 22 | hosts = ['api.example.com'] 23 | [unit.action] 24 | proxy = 'http://127.0.0.1:8222' 25 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1719994518, 9 | "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "flake-utils": { 22 | "inputs": { 23 | "systems": "systems" 24 | }, 25 | "locked": { 26 | "lastModified": 1710146030, 27 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "numtide", 35 | "repo": "flake-utils", 36 | "type": "github" 37 | } 38 | }, 39 | "flake-utils_2": { 40 | "inputs": { 41 | "systems": "systems_2" 42 | }, 43 | "locked": { 44 | "lastModified": 1705309234, 45 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 46 | "owner": "numtide", 47 | "repo": "flake-utils", 48 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "numtide", 53 | "repo": "flake-utils", 54 | "type": "github" 55 | } 56 | }, 57 | "nixpkgs": { 58 | "locked": { 59 | "lastModified": 1714253743, 60 | "narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=", 61 | "owner": "NixOS", 62 | "repo": "nixpkgs", 63 | "rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994", 64 | "type": "github" 65 | }, 66 | "original": { 67 | "owner": "NixOS", 68 | "ref": "nixos-unstable", 69 | "repo": "nixpkgs", 70 | "type": "github" 71 | } 72 | }, 73 | "nixpkgs-lib": { 74 | "locked": { 75 | "lastModified": 1719876945, 76 | "narHash": "sha256-Fm2rDDs86sHy0/1jxTOKB1118Q0O3Uc7EC0iXvXKpbI=", 77 | "type": "tarball", 78 | "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" 79 | }, 80 | "original": { 81 | "type": "tarball", 82 | "url": "https://github.com/NixOS/nixpkgs/archive/5daf0514482af3f97abaefc78a6606365c9108e2.tar.gz" 83 | } 84 | }, 85 | "nixpkgs_2": { 86 | "locked": { 87 | "lastModified": 1706487304, 88 | "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", 89 | "owner": "NixOS", 90 | "repo": "nixpkgs", 91 | "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", 92 | "type": "github" 93 | }, 94 | "original": { 95 | "owner": "NixOS", 96 | "ref": "nixpkgs-unstable", 97 | "repo": "nixpkgs", 98 | "type": "github" 99 | } 100 | }, 101 | "root": { 102 | "inputs": { 103 | "flake-parts": "flake-parts", 104 | "flake-utils": "flake-utils", 105 | "nixpkgs": "nixpkgs", 106 | "rust-overlay": "rust-overlay" 107 | } 108 | }, 109 | "rust-overlay": { 110 | "inputs": { 111 | "flake-utils": "flake-utils_2", 112 | "nixpkgs": "nixpkgs_2" 113 | }, 114 | "locked": { 115 | "lastModified": 1714616033, 116 | "narHash": "sha256-JcWAjIDl3h0bE/pII0emeHwokTeBl+SWrzwrjoRu7a0=", 117 | "owner": "oxalica", 118 | "repo": "rust-overlay", 119 | "rev": "3e416d5067ba31ff8ac31eeb763e4388bdf45089", 120 | "type": "github" 121 | }, 122 | "original": { 123 | "owner": "oxalica", 124 | "repo": "rust-overlay", 125 | "type": "github" 126 | } 127 | }, 128 | "systems": { 129 | "locked": { 130 | "lastModified": 1681028828, 131 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 132 | "owner": "nix-systems", 133 | "repo": "default", 134 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 135 | "type": "github" 136 | }, 137 | "original": { 138 | "owner": "nix-systems", 139 | "repo": "default", 140 | "type": "github" 141 | } 142 | }, 143 | "systems_2": { 144 | "locked": { 145 | "lastModified": 1681028828, 146 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 147 | "owner": "nix-systems", 148 | "repo": "default", 149 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 150 | "type": "github" 151 | }, 152 | "original": { 153 | "owner": "nix-systems", 154 | "repo": "default", 155 | "type": "github" 156 | } 157 | } 158 | }, 159 | "root": "root", 160 | "version": 7 161 | } 162 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Jucenit - A simple web server"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | flake-parts.url = "github:hercules-ci/flake-parts"; 9 | }; 10 | 11 | outputs = { 12 | self, 13 | nixpkgs, 14 | rust-overlay, 15 | flake-utils, 16 | flake-parts, 17 | } @ inputs: 18 | flake-parts.lib.mkFlake { 19 | inherit inputs; 20 | } { 21 | flake = { 22 | nixosModules = { 23 | jucenit = ./module.nix; 24 | }; 25 | }; 26 | 27 | systems = 28 | flake-utils.lib.allSystems; 29 | perSystem = { 30 | config, 31 | self, 32 | inputs, 33 | pkgs, 34 | system, 35 | ... 36 | }: { 37 | packages.default = pkgs.callPackage ./package.nix {}; 38 | devShells.default = pkgs.callPackage ./shell.nix {}; 39 | }; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /jucenit.toml: -------------------------------------------------------------------------------- 1 | [[unit]] 2 | uuid = "myuuid" 3 | listeners = ["*:443"] 4 | 5 | [unit.match] 6 | hosts = ["example.com"] 7 | 8 | [unit.action] 9 | proxy = "http://127.0.0.1:8888" 10 | -------------------------------------------------------------------------------- /jucenit/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jucenit" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | miette = { version = "7.2.0", features = ["fancy"] } 10 | cli = { path = "../cli" } 11 | log = "0.4.21" 12 | tokio = { version = "1.37.0", features = ["full"] } 13 | -------------------------------------------------------------------------------- /jucenit/src/main.rs: -------------------------------------------------------------------------------- 1 | // Error Handling 2 | use log::trace; 3 | use miette::Result; 4 | // Clap 5 | use cli::Cli; 6 | 7 | /** 8 | The jucenit binary entrypoint. 9 | This main function is the first function to be executed when launching pipelight. 10 | */ 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | trace!("Launch process."); 14 | make_handler()?; 15 | Cli::run().await?; 16 | trace!("Process clean exit."); 17 | Ok(()) 18 | } 19 | 20 | /** 21 | The make handler functions is executed right after the main function 22 | to set up a verbose and colorful error/panic handler. 23 | */ 24 | pub fn make_handler() -> Result<()> { 25 | miette::set_panic_hook(); 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /jucenit_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jucenit_core" 3 | version = "0.2.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | pipelight = { git = "https://github.com/pipelight/pipelight", branch = "dev" } # utils = { git = "https://github.com/pipelight/pipelight?ref=dev" } 8 | # acme2 = "0.5.1" 9 | acme2 = { git = "https://github.com/pipelight/acme2" } 10 | migration = { path = "../migration" } 11 | colored = "2.1.0" 12 | miette = { version = "7.2.0", features = ["fancy"] } 13 | serde = { version = "1.0.200", features = ["derive"] } 14 | serde_json = "1.0.116" 15 | serde_yaml = "0.9.34" 16 | thiserror = "1.0.59" 17 | toml = "0.8.12" 18 | tokio = { version = "1.37.0", features = ["full"] } 19 | reqwest = { version = "0.12.4", features = ["json", "h3", "brotli"] } 20 | once_cell = "1.19.0" 21 | uuid = "1.8.0" 22 | rcgen = "0.13.1" 23 | http = "1.1.0" 24 | openssl = "0.10.64" 25 | chrono = { version = "0.4.38", features = ["serde"] } 26 | futures = "0.3.30" 27 | rayon = "1.10.0" 28 | indexmap = { version = "2.2.6", features = ["serde", "rayon"] } 29 | sea-orm = { version = "0.12.15", features = [ 30 | "runtime-tokio-rustls", 31 | "sqlx-sqlite", 32 | "macros", 33 | "mock", 34 | ] } 35 | tracing = { version = "0.1.40", features = ["log", "async-await"] } 36 | strum = { version = "0.26.3", features = ["derive"] } 37 | 38 | [dev-dependencies] 39 | serial_test = "3.1.1" 40 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/config.rs: -------------------------------------------------------------------------------- 1 | use crate::nginx::Config as NginxConfig; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | // Error Handling 5 | use crate::error::{TomlError, YamlError}; 6 | use miette::{Error, IntoDiagnostic, Result}; 7 | // Filesystem 8 | use std::fs; 9 | use std::path::Path; 10 | 11 | // use utils::{files::FileType, teleport::Portal}; 12 | use pipelight::utils::{error::PipelightError, files::FileType, teleport::Portal}; 13 | 14 | // Config file related structs 15 | /** 16 | * Config file 17 | */ 18 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 19 | #[serde(deny_unknown_fields)] 20 | pub struct Config { 21 | pub unit: Vec, 22 | } 23 | 24 | impl Config { 25 | /** 26 | * Search the filesystem for a config file. 27 | */ 28 | pub fn get() -> Result { 29 | let mut portal = Portal::new().into_diagnostic()?; 30 | portal.seed("jucenit"); 31 | let res = portal.search().into_diagnostic()?; 32 | let config = Config::load(&portal.target.file_path.unwrap())?; 33 | Ok(config) 34 | } 35 | 36 | /** 37 | * Choose the appropriated method to load the config file 38 | * according to the file extension(.toml or .yml). 39 | * 40 | * Arguments: 41 | * - file_path is the config file path 42 | * - args are only to be used with scripting language (typescript) to pass args to the underlying script. 43 | */ 44 | pub fn load(file_path: &str) -> Result { 45 | // TODO: add Hcl and Kcl. 46 | let extension = &Path::new(file_path) 47 | .extension() 48 | .unwrap() 49 | .to_str() 50 | .unwrap() 51 | .to_owned(); 52 | 53 | let file_type = FileType::from(extension); 54 | let config = match file_type { 55 | FileType::Toml | FileType::Tml => Config::from_toml_file(file_path)?, 56 | FileType::Yaml | FileType::Yml => Config::from_yaml_file(file_path)?, 57 | _ => { 58 | let msg = format!("File type is unknown"); 59 | return Err(Error::msg(msg)); 60 | } 61 | }; 62 | Ok(config) 63 | } 64 | /** 65 | Returns a jucenit configuration from a provided toml file path. 66 | */ 67 | pub fn from_toml_file(file_path: &str) -> Result { 68 | let tml = fs::read_to_string(file_path).into_diagnostic()?; 69 | let res = toml::from_str::(&tml); 70 | match res { 71 | Ok(res) => Ok(res), 72 | Err(e) => { 73 | let err = TomlError::new(e, &tml); 74 | Err(err.into()) 75 | } 76 | } 77 | } 78 | /** 79 | Returns a jucenit configuration from a provided toml string. 80 | */ 81 | pub fn from_toml_str(toml: &str) -> Result { 82 | let res = toml::from_str::(&toml); 83 | match res { 84 | Ok(res) => Ok(res), 85 | Err(e) => { 86 | let err = TomlError::new(e, toml); 87 | Err(err.into()) 88 | } 89 | } 90 | } 91 | pub fn to_toml(&self) -> Result { 92 | let res = toml::to_string_pretty(self).into_diagnostic(); 93 | res 94 | } 95 | /** 96 | * Returns a jucenit configuration from a provided yaml file path. 97 | */ 98 | pub fn from_yaml_file(file_path: &str) -> Result { 99 | let yml = fs::read_to_string(file_path).into_diagnostic()?; 100 | let res = serde_yaml::from_str::(&yml); 101 | match res { 102 | Ok(res) => Ok(res), 103 | Err(e) => { 104 | let err = YamlError::new(e, &yml); 105 | Err(err.into()) 106 | } 107 | } 108 | } 109 | /** 110 | Returns a jucenit configuration from a provided yaml string. 111 | */ 112 | pub fn from_yaml_str(yml: &str) -> Result { 113 | let res = serde_yaml::from_str::(&yml); 114 | match res { 115 | Ok(res) => Ok(res), 116 | Err(e) => { 117 | let err = YamlError::new(e, &yml); 118 | Err(err.into()) 119 | } 120 | } 121 | } 122 | pub fn to_yaml(&self) -> Result { 123 | let res = serde_yaml::to_string(self).into_diagnostic(); 124 | res 125 | } 126 | } 127 | 128 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 129 | #[serde(deny_unknown_fields)] 130 | pub struct Unit { 131 | #[serde(skip_serializing_if = "Option::is_none")] 132 | pub id: Option, 133 | pub uuid: String, 134 | pub action: Option, 135 | #[serde(rename = "match")] 136 | pub match_: Match, 137 | pub listeners: Vec, 138 | } 139 | impl Unit { 140 | /** 141 | Returns a jucenit configuration from a provided toml string. 142 | */ 143 | pub fn from_toml_str(toml: &str) -> Result { 144 | let res = toml::from_str::(&toml); 145 | match res { 146 | Ok(res) => Ok(res), 147 | Err(e) => { 148 | let err = TomlError::new(e, toml); 149 | Err(err.into()) 150 | } 151 | } 152 | } 153 | pub fn to_toml(&self) -> Result { 154 | let res = toml::to_string_pretty(self).into_diagnostic(); 155 | res 156 | } 157 | /** 158 | Returns a jucenit configuration from a provided yaml string. 159 | */ 160 | pub fn from_yaml_str(yml: &str) -> Result { 161 | let res = serde_yaml::from_str::(&yml); 162 | match res { 163 | Ok(res) => Ok(res), 164 | Err(e) => { 165 | let err = YamlError::new(e, &yml); 166 | Err(err.into()) 167 | } 168 | } 169 | } 170 | pub fn to_yaml(&self) -> Result { 171 | let res = serde_yaml::to_string(self).into_diagnostic(); 172 | res 173 | } 174 | } 175 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 176 | pub struct Action { 177 | // Reverse proxy 178 | #[serde(skip_serializing_if = "Option::is_none")] 179 | #[serde(flatten)] 180 | pub raw_params: Option, 181 | } 182 | 183 | #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] 184 | pub struct Match { 185 | #[serde(skip_serializing_if = "Option::is_none")] 186 | pub hosts: Option>, 187 | 188 | #[serde(skip_serializing_if = "Option::is_none")] 189 | #[serde(flatten)] 190 | pub raw_params: Option, 191 | } 192 | 193 | #[cfg(test)] 194 | mod tests { 195 | use super::Config as ConfigFile; 196 | use miette::Result; 197 | 198 | #[test] 199 | fn get_from_toml_file() -> Result<()> { 200 | let res = ConfigFile::from_toml_file("../examples/jucenit.toml")?; 201 | println!("{:#?}", res); 202 | Ok(()) 203 | } 204 | #[test] 205 | fn seek_a_config_file() -> Result<()> { 206 | let res = ConfigFile::get()?; 207 | println!("{:#?}", res); 208 | Ok(()) 209 | } 210 | #[test] 211 | fn get_from_toml_string() -> Result<()> { 212 | let toml = " 213 | [[unit]] 214 | uuid = 'd3630938-5851-43ab-a523-84e0c6af9eb1' 215 | listeners = ['*:443'] 216 | [unit.match] 217 | hosts = ['test.com', 'example.com'] 218 | [unit.action] 219 | proxy = 'http://127.0.0.1:8333' 220 | "; 221 | let res = ConfigFile::from_toml_str(toml)?; 222 | println!("{:#?}", res); 223 | Ok(()) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/from.rs: -------------------------------------------------------------------------------- 1 | use super::{Action, Match}; 2 | 3 | use crate::database::entity::{prelude::*, *}; 4 | use sea_orm::{prelude::*, sea_query::OnConflict, ActiveValue, InsertResult}; 5 | 6 | impl Action { 7 | pub fn from(e: &action::Model) -> Action { 8 | let action = Action { 9 | raw_params: serde_json::from_str(&e.raw_params).unwrap(), 10 | }; 11 | action 12 | } 13 | } 14 | 15 | impl Match { 16 | pub fn from(e: &ng_match::Model, hosts: Vec) -> Self { 17 | let domain_names: Vec = hosts.iter().map(|x| x.clone().domain).collect(); 18 | let hosts; 19 | if domain_names.is_empty() { 20 | hosts = None; 21 | } else { 22 | hosts = Some(domain_names) 23 | } 24 | let match_ = Self { 25 | hosts, 26 | raw_params: e 27 | .raw_params 28 | .clone() 29 | .map(|x| serde_json::from_str(&x).unwrap()), 30 | }; 31 | match_ 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/from_database.rs: -------------------------------------------------------------------------------- 1 | // Database 2 | use super::config; 3 | use crate::database::{connect_db, fresh_db}; 4 | use crate::{ConfigFile, ConfigUnit, NginxConfig}; 5 | // Sea orm 6 | // use indexmap::IndexMap; 7 | use crate::database::entity::{prelude::*, *}; 8 | use rayon::iter::Update; 9 | use sea_orm::{ 10 | prelude::*, query::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase, 11 | }; 12 | use sea_orm::{Database, DatabaseConnection}; 13 | // Logging 14 | use tracing::{debug, Level}; 15 | // Error Handling 16 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 17 | 18 | // Fs 19 | use std::env; 20 | use std::process::{Command, Stdio}; 21 | use tokio::fs; 22 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 23 | 24 | impl ConfigFile { 25 | pub async fn pull() -> Result { 26 | let db = connect_db().await?; 27 | 28 | let mut config = ConfigFile::default(); 29 | 30 | let matches = NgMatch::find().all(&db).await.into_diagnostic()?; 31 | 32 | for match_ in matches { 33 | let action = match_ 34 | .find_related(Action) 35 | .one(&db) 36 | .await 37 | .into_diagnostic()?; 38 | let hosts = match_.find_related(Host).all(&db).await.into_diagnostic()?; 39 | let listeners = match_ 40 | .find_related(Listener) 41 | .all(&db) 42 | .await 43 | .into_diagnostic()?; 44 | 45 | let unit = ConfigUnit { 46 | uuid: match_.clone().uuid, 47 | action: Some(config::Action::from(&action.unwrap())), 48 | match_: config::Match::from(&match_, hosts), 49 | listeners: listeners.iter().map(|x| x.clone().ip_socket).collect(), 50 | ..Default::default() 51 | }; 52 | 53 | config.unit.push(unit); 54 | } 55 | Ok(config) 56 | } 57 | pub async fn edit(&self) -> Result<()> { 58 | let tmp_dir = "/tmp/jucenit"; 59 | fs::create_dir_all(tmp_dir).await.into_diagnostic()?; 60 | let path = "/tmp/jucenit/jucenit.config.tmp.toml".to_owned(); 61 | 62 | // Retrieve config 63 | let toml = ConfigFile::pull().await?.to_toml()?; 64 | // Create and write to file 65 | let mut file = fs::File::create(path.clone()).await.into_diagnostic()?; 66 | let bytes = toml.as_bytes(); 67 | file.write_all(bytes).await.into_diagnostic()?; 68 | 69 | // Modify file with editor 70 | let editor = env::var("EDITOR").into_diagnostic()?; 71 | let child = Command::new(editor) 72 | .arg(path.clone()) 73 | .stdin(Stdio::null()) 74 | .stdout(Stdio::inherit()) 75 | .stderr(Stdio::inherit()) 76 | .spawn() 77 | .expect("Couldn't spawn a detached subprocess"); 78 | let output = child.wait_with_output().into_diagnostic()?; 79 | 80 | // Try Update nginx-unit config 81 | let tmp_config = ConfigFile::load(&path)?; 82 | tmp_config.set().await?; 83 | 84 | // Clean up tmp files before exit 85 | fs::remove_file(path).await.into_diagnostic()?; 86 | Ok(()) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod test { 92 | use crate::database::entity::{prelude::*, *}; 93 | use crate::database::{connect_db, fresh_db}; 94 | use crate::{ConfigFile, Match, NginxConfig}; 95 | use sea_orm::{prelude::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase}; 96 | // Logging 97 | use tracing::{debug, Level}; 98 | // Error Handling 99 | use miette::{IntoDiagnostic, Result}; 100 | use std::path::PathBuf; 101 | /** 102 | * Set a fresh testing environment: 103 | * - clean certificate store 104 | * - set minimal nginx configuration 105 | */ 106 | async fn set_testing_config() -> Result<()> { 107 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 108 | path.push("../examples/jucenit.toml"); 109 | 110 | let config = ConfigFile::load(path.to_str().unwrap())?; 111 | config.set().await?; 112 | 113 | Ok(()) 114 | } 115 | 116 | #[tokio::test] 117 | async fn get_config() -> Result<()> { 118 | set_testing_config().await?; 119 | ConfigFile::pull().await?; 120 | Ok(()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod from; 3 | pub mod from_database; 4 | pub mod to_database; 5 | 6 | // Public Reexport 7 | pub use config::*; 8 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/to_database/delete.rs: -------------------------------------------------------------------------------- 1 | // Database 2 | use crate::database::{connect_db, fresh_db}; 3 | use crate::{ConfigFile, ConfigUnit, NginxConfig}; 4 | // Sea orm 5 | // use indexmap::IndexMap; 6 | use crate::database::entity::{prelude::*, *}; 7 | use migration::{Migrator, MigratorTrait}; 8 | use sea_orm::{prelude::*, query::*, sea_query::OnConflict, ActiveValue, InsertResult}; 9 | use sea_orm::{Database, DatabaseConnection}; 10 | use serde_json::json; 11 | // Logging 12 | use tracing::{debug, Level}; 13 | // Error Handling 14 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 15 | 16 | impl ConfigFile { 17 | pub async fn remove(&self) -> Result<()> { 18 | self.remove_from_db().await?; 19 | let nginx_config = NginxConfig::pull().await?; 20 | nginx_config.set().await?; 21 | Ok(()) 22 | } 23 | pub async fn remove_from_db(&self) -> Result<()> { 24 | for unit in &self.unit { 25 | unit.remove_from_db().await?; 26 | } 27 | Ok(()) 28 | } 29 | pub async fn purge_http_challenge() -> Result<()> { 30 | let db = connect_db().await?; 31 | 32 | let like = json!({"uri":[]}); 33 | let like = like.to_string(); 34 | 35 | let matches = NgMatch::find().all(&db).await.into_diagnostic()?; 36 | let challenges: Vec<&ng_match::Model> = matches 37 | .iter() 38 | .filter(|x| { 39 | if let Some(raw_params) = x.raw_params.clone() { 40 | let raw_params: serde_json::Value = serde_json::from_str(&raw_params).unwrap(); 41 | return raw_params["uri"] 42 | .to_string() 43 | .contains("/.well-known/acme-challenge/"); 44 | } 45 | return false; 46 | }) 47 | .collect(); 48 | 49 | for x in challenges { 50 | let x = x.clone(); 51 | x.delete(&db).await.into_diagnostic()?; 52 | } 53 | Ok(()) 54 | } 55 | } 56 | impl ConfigUnit { 57 | pub async fn remove(&self) -> Result<()> { 58 | self.remove_from_db().await?; 59 | let nginx_config = NginxConfig::pull().await?; 60 | nginx_config.set().await?; 61 | Ok(()) 62 | } 63 | pub async fn remove_from_db(&self) -> Result<()> { 64 | let unit = self; 65 | let db = connect_db().await?; 66 | 67 | let match_ = NgMatch::find() 68 | .filter(Condition::all().add(ng_match::Column::Uuid.eq(&unit.uuid))) 69 | .one(&db) 70 | .await 71 | .into_diagnostic()?; 72 | 73 | if let Some(match_) = match_ { 74 | let hosts = match_.find_related(Host).all(&db).await.into_diagnostic()?; 75 | for host in hosts { 76 | // Delete host if not linked to other matches. 77 | if host 78 | .find_related(NgMatch) 79 | .filter( 80 | Condition::all() 81 | .not() 82 | .add(ng_match::Column::Uuid.eq(&unit.uuid)), 83 | ) 84 | .all(&db) 85 | .await 86 | .into_diagnostic()? 87 | .is_empty() 88 | { 89 | host.delete(&db).await.into_diagnostic()?; 90 | } 91 | } 92 | let action = match_ 93 | .find_related(Action) 94 | .one(&db) 95 | .await 96 | .into_diagnostic()?; 97 | let action = action.unwrap(); 98 | 99 | let listeners = match_ 100 | .find_related(Listener) 101 | .all(&db) 102 | .await 103 | .into_diagnostic()?; 104 | for listener in listeners { 105 | // Delete listeners if no related match 106 | if listener 107 | .find_related(NgMatch) 108 | .filter( 109 | Condition::all() 110 | .not() 111 | .add(ng_match::Column::Uuid.eq(&unit.uuid)), 112 | ) 113 | .all(&db) 114 | .await 115 | .into_diagnostic()? 116 | .is_empty() 117 | { 118 | listener.delete(&db).await.into_diagnostic()?; 119 | } 120 | } 121 | 122 | // Delete action if not linked to other matches. 123 | let mut del_action = false; 124 | if action 125 | .find_related(NgMatch) 126 | .filter( 127 | Condition::all() 128 | .not() 129 | .add(ng_match::Column::Uuid.eq(&unit.uuid)), 130 | ) 131 | .all(&db) 132 | .await 133 | .into_diagnostic()? 134 | .is_empty() 135 | { 136 | del_action = true; 137 | } 138 | 139 | match_.delete(&db).await.into_diagnostic()?; 140 | // Delete action after match (fk constraint) 141 | if del_action { 142 | action.delete(&db).await.into_diagnostic()?; 143 | } 144 | } 145 | Ok(()) 146 | } 147 | } 148 | #[cfg(test)] 149 | mod test { 150 | use crate::database::entity::{prelude::*, *}; 151 | use crate::database::{connect_db, fresh_db}; 152 | use crate::{ConfigFile, ConfigUnit, Match, Nginx, NginxConfig}; 153 | use sea_orm::{prelude::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase}; 154 | use std::path::PathBuf; 155 | // Logging 156 | use tracing::{debug, Level}; 157 | // Error Handling 158 | use miette::{IntoDiagnostic, Result}; 159 | 160 | /** 161 | * Set a fresh testing environment: 162 | * - clean certificate store 163 | * - set minimal nginx configuration 164 | */ 165 | async fn set_testing_config() -> Result<()> { 166 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 167 | path.push("../examples/jucenit.toml"); 168 | 169 | let config = ConfigFile::load(path.to_str().unwrap())?; 170 | config.set().await?; 171 | 172 | Ok(()) 173 | } 174 | 175 | #[tokio::test] 176 | async fn remove_http_challenges() -> Result<()> { 177 | set_testing_config().await?; 178 | let toml = " 179 | uuid = 'random-uuid' 180 | listeners = ['*:80'] 181 | [match] 182 | hosts = ['test.com'] 183 | uri = ['/.well-known/acme-challenge/uuid'] 184 | [action] 185 | share = ['/tmp/jucenit/challenge_uuid.txt'] 186 | "; 187 | let unit = ConfigUnit::from_toml_str(&toml)?; 188 | unit.push().await?; 189 | 190 | ConfigFile::purge_http_challenge().await?; 191 | 192 | Ok(()) 193 | } 194 | #[tokio::test] 195 | async fn remove_unit_by_uuid() -> Result<()> { 196 | set_testing_config().await?; 197 | let toml = " 198 | [[unit]] 199 | uuid = 'd3630938-5851-43ab-a523-84e0c6af9eb1' 200 | listeners = ['*:443'] 201 | [unit.match] 202 | hosts = ['test.com', 'example.com'] 203 | [unit.action] 204 | proxy = 'http://127.0.0.1:8333' 205 | "; 206 | let config = ConfigFile::from_toml_str(toml)?; 207 | config.remove().await?; 208 | // let nginx_config = NginxConfig::pull().await?; 209 | // println!("{:#?}", nginx_config); 210 | Ok(()) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/to_database/insert.rs: -------------------------------------------------------------------------------- 1 | // Database 2 | use crate::database::{connect_db, fresh_db}; 3 | use crate::{ConfigFile, ConfigUnit, NginxConfig}; 4 | // Sea orm 5 | // use indexmap::IndexMap; 6 | use crate::database::entity::{prelude::*, *}; 7 | use migration::{Migrator, MigratorTrait}; 8 | use rayon::iter::Update; 9 | use sea_orm::{ 10 | prelude::*, query::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase, 11 | }; 12 | use sea_orm::{Database, DatabaseConnection}; 13 | // Logging 14 | use tracing::{debug, Level}; 15 | // Error Handling 16 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 17 | 18 | impl ConfigFile { 19 | /** 20 | * Push file to database 21 | */ 22 | pub async fn push_to_db(&self) -> Result<()> { 23 | for unit in &self.unit { 24 | unit.push_to_db().await?; 25 | } 26 | Ok(()) 27 | } 28 | /** 29 | * Clean up database and push file to database 30 | */ 31 | async fn push_to_fresh_db(&self) -> Result<()> { 32 | fresh_db().await?; 33 | for unit in &self.unit { 34 | unit.push_to_db().await?; 35 | } 36 | Ok(()) 37 | } 38 | /** 39 | * Push file to database 40 | * and update nginx 41 | */ 42 | pub async fn push(&self) -> Result<()> { 43 | self.push_to_db().await?; 44 | let nginx_config = NginxConfig::pull().await?; 45 | nginx_config.set().await?; 46 | Ok(()) 47 | } 48 | /** 49 | * Clean up database and push file to database 50 | * and update nginx 51 | */ 52 | pub async fn set(&self) -> Result<()> { 53 | self.push_to_fresh_db().await?; 54 | let nginx_config = NginxConfig::pull().await?; 55 | nginx_config.set().await?; 56 | Ok(()) 57 | } 58 | } 59 | impl ConfigUnit { 60 | pub async fn push(&self) -> Result<()> { 61 | self.push_to_db().await?; 62 | let nginx_config = NginxConfig::pull().await?; 63 | nginx_config.set().await?; 64 | Ok(()) 65 | } 66 | pub async fn push_to_db(&self) -> Result<()> { 67 | let unit = self; 68 | // Logic Guards 69 | // Ignore gracefully if matching pattern lakes parameters 70 | if unit.match_.raw_params.is_none() 71 | || unit.match_.raw_params == Some(serde_json::from_str("{}").into_diagnostic()?) 72 | && unit.match_.hosts.is_none() 73 | { 74 | return Ok(()); 75 | } 76 | // Ignore gracefully if no action 77 | if let Some(action) = &unit.action { 78 | if action.raw_params == Some(serde_json::from_str("{}").into_diagnostic()?) { 79 | return Ok(()); 80 | } 81 | } 82 | 83 | let db = connect_db().await?; 84 | 85 | // Insert Action 86 | let raw_params: String = unit.action.clone().unwrap().raw_params.unwrap().to_string(); 87 | 88 | let mut action = action::ActiveModel { 89 | raw_params: ActiveValue::Set(raw_params.clone()), 90 | ..Default::default() 91 | }; 92 | 93 | let res = Action::insert(action) 94 | .on_conflict( 95 | OnConflict::column(action::Column::RawParams) 96 | .do_nothing() 97 | .to_owned(), 98 | ) 99 | .exec_with_returning(&db) 100 | .await 101 | .into_diagnostic(); 102 | 103 | // Populate entities with ids 104 | action = match res { 105 | Ok(model) => model.into(), 106 | Err(e) => { 107 | // debug!("{}", e); 108 | // println!("{}", e); 109 | let model = Action::find() 110 | .filter(action::Column::RawParams.eq(raw_params)) 111 | .one(&db) 112 | .await 113 | .into_diagnostic()? 114 | .unwrap() 115 | .into(); 116 | model 117 | } 118 | }; 119 | 120 | // Insert Match 121 | let raw_params: Option = unit.match_.clone().raw_params.map(|x| x.to_string()); 122 | let mut match_ = ng_match::ActiveModel { 123 | uuid: ActiveValue::Set(unit.uuid.clone()), 124 | action_id: action.id, 125 | raw_params: ActiveValue::Set(raw_params.clone()), 126 | ..Default::default() 127 | }; 128 | let res = NgMatch::insert(match_) 129 | .on_conflict( 130 | OnConflict::column(ng_match::Column::Uuid) 131 | .do_nothing() 132 | .to_owned(), 133 | ) 134 | .exec_with_returning(&db) 135 | .await 136 | .into_diagnostic(); 137 | 138 | // Return the existing entity 139 | match_ = match res { 140 | Ok(model) => model.into(), 141 | Err(e) => { 142 | // debug!("{}", e); 143 | // println!("{}", e); 144 | // println!("{:#?}", raw_params); 145 | let model = NgMatch::find() 146 | .filter(ng_match::Column::Uuid.eq(&unit.uuid)) 147 | .one(&db) 148 | .await 149 | .into_diagnostic()? 150 | .unwrap() 151 | .into(); 152 | model 153 | } 154 | }; 155 | 156 | // Insert listeners 157 | assert!(!&unit.listeners.is_empty()); 158 | let mut listeners: Vec = vec![]; 159 | for l in &unit.listeners { 160 | let listener = listener::ActiveModel { 161 | ip_socket: ActiveValue::Set(l.to_owned()), 162 | ..Default::default() 163 | }; 164 | listeners.push(listener); 165 | } 166 | let res = Listener::insert_many(listeners.clone()) 167 | .on_conflict( 168 | OnConflict::column(listener::Column::IpSocket) 169 | .update_column(listener::Column::Tls) 170 | .do_nothing() 171 | .to_owned(), 172 | ) 173 | .do_nothing() 174 | .exec_without_returning(&db) 175 | .await 176 | .into_diagnostic()?; 177 | 178 | // Populate entities with ids 179 | let models = Listener::find() 180 | .filter(listener::Column::IpSocket.is_in(&unit.listeners)) 181 | .all(&db) 182 | .await 183 | .into_diagnostic()?; 184 | listeners = models 185 | .iter() 186 | .map(|x| listener::ActiveModel::from(x.to_owned())) 187 | .collect(); 188 | 189 | // Join Match and Listener 190 | assert!(!&listeners.is_empty()); 191 | let mut list: Vec = vec![]; 192 | for listener in listeners { 193 | let match_listener = match_listener::ActiveModel { 194 | match_id: match_.id.clone(), 195 | listener_id: listener.id, 196 | }; 197 | list.push(match_listener) 198 | } 199 | let _ = MatchListener::insert_many(list) 200 | .on_conflict( 201 | OnConflict::columns(vec![ 202 | match_listener::Column::MatchId, 203 | match_listener::Column::ListenerId, 204 | ]) 205 | .do_nothing() 206 | .to_owned(), 207 | ) 208 | .do_nothing() 209 | .exec(&db) 210 | .await 211 | .into_diagnostic()?; 212 | 213 | // Insert Hosts 214 | if unit.match_.hosts.is_some() { 215 | let mut hosts: Vec = vec![]; 216 | if let Some(dns) = &unit.match_.hosts { 217 | for host in dns { 218 | let host = host::ActiveModel { 219 | domain: ActiveValue::Set(host.to_owned()), 220 | ..Default::default() 221 | }; 222 | hosts.push(host); 223 | } 224 | let res = Host::insert_many(hosts.clone()) 225 | .on_conflict( 226 | OnConflict::column(host::Column::Domain) 227 | .do_nothing() 228 | .to_owned(), 229 | ) 230 | .do_nothing() 231 | .exec_without_returning(&db) 232 | .await 233 | .into_diagnostic(); 234 | // Populate entities with ids 235 | // debug!("{}", e); 236 | // println!("{}", e); 237 | let models = Host::find() 238 | .filter(host::Column::Domain.is_in(dns)) 239 | .all(&db) 240 | .await 241 | .into_diagnostic()?; 242 | hosts = models 243 | .iter() 244 | .map(|x| host::ActiveModel::from(x.to_owned())) 245 | .collect(); 246 | }; 247 | 248 | // Join Match and Host 249 | let mut list: Vec = vec![]; 250 | for host in hosts { 251 | let match_host = match_host::ActiveModel { 252 | match_id: match_.id.clone(), 253 | host_id: host.id, 254 | }; 255 | list.push(match_host) 256 | } 257 | let _ = MatchHost::insert_many(list) 258 | .on_conflict( 259 | OnConflict::columns(vec![ 260 | match_host::Column::MatchId, 261 | match_host::Column::HostId, 262 | ]) 263 | .do_nothing() 264 | .to_owned(), 265 | ) 266 | .do_nothing() 267 | .exec(&db) 268 | .await 269 | .into_diagnostic()?; 270 | } 271 | Ok(()) 272 | } 273 | } 274 | 275 | #[cfg(test)] 276 | mod test { 277 | use crate::database::entity::{prelude::*, *}; 278 | use crate::database::{connect_db, fresh_db}; 279 | use crate::{ConfigFile, Match, NginxConfig}; 280 | use sea_orm::{prelude::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase}; 281 | // Logging 282 | use tracing::{debug, Level}; 283 | // Error Handling 284 | use miette::{IntoDiagnostic, Result}; 285 | use std::path::PathBuf; 286 | 287 | /** 288 | * Set a fresh testing environment: 289 | * - clean certificate store 290 | * - set minimal nginx configuration 291 | */ 292 | async fn set_testing_config() -> Result<()> { 293 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 294 | path.push("../examples/jucenit.toml"); 295 | 296 | let config = ConfigFile::load(path.to_str().unwrap())?; 297 | config.set().await?; 298 | 299 | Ok(()) 300 | } 301 | 302 | #[tokio::test] 303 | async fn seed_db() -> Result<()> { 304 | set_testing_config().await?; 305 | Ok(()) 306 | } 307 | 308 | #[tokio::test] 309 | async fn salve_push() -> Result<()> { 310 | set_testing_config().await?; 311 | 312 | let toml = " 313 | [[unit]] 314 | uuid = 'cc4e626a-9354-480e-a78b-f9f845148984' 315 | listeners = ['*:443'] 316 | [unit.match] 317 | hosts = ['api.example.com'] 318 | [unit.action] 319 | proxy = 'http://127.0.0.1:8222' 320 | "; 321 | 322 | let config = ConfigFile::from_toml_str(toml)?; 323 | config.push().await?; 324 | 325 | Ok(()) 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/to_database/methods.rs: -------------------------------------------------------------------------------- 1 | use strum::EnumIter; 2 | 3 | #[derive(EnumIter)] 4 | pub enum MatchCategory { 5 | Managed, 6 | Unmanaged, 7 | HttpChallenge, 8 | TlsAlpnChallenge, 9 | } 10 | -------------------------------------------------------------------------------- /jucenit_core/src/cast/to_database/mod.rs: -------------------------------------------------------------------------------- 1 | mod delete; 2 | mod insert; 3 | mod methods; 4 | 5 | // Reexports 6 | pub use delete::*; 7 | pub use insert::*; 8 | pub use methods::*; 9 | -------------------------------------------------------------------------------- /jucenit_core/src/database/crud.rs: -------------------------------------------------------------------------------- 1 | use crate::cast::Config as ConfigFile; 2 | use crate::{Action, Match}; 3 | use indexmap::IndexMap; 4 | use serde::{Deserialize, Serialize}; 5 | // File 6 | use crate::nginx::Config as NginxConfig; 7 | use std::env; 8 | use std::os::unix::fs::PermissionsExt; 9 | use std::path::Path; 10 | use std::process::{Command, Stdio}; 11 | use tokio::fs; 12 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 13 | 14 | // Sea orm 15 | // use indexmap::IndexMap; 16 | use super::entity::{prelude::*, *}; 17 | use migration::{Migrator, MigratorTrait}; 18 | use sea_orm::{ 19 | error::{ConnAcquireErr, DbErr}, 20 | Database, DatabaseConnection, 21 | }; 22 | use sea_orm::{prelude::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase}; 23 | 24 | // Error Handling 25 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 26 | 27 | // Global vars 28 | // use once_cell::sync::Lazy; 29 | // use std::sync::Arc; 30 | // use tokio::sync::Mutex; 31 | 32 | pub async fn connect_db() -> Result { 33 | let database_url = "sqlite:////var/spool/jucenit/config.sqlite?mode=rw"; 34 | // let db: DatabaseConnection = Database::connect(database_url).await.into_diagnostic()?; 35 | let db = Database::connect(database_url).await; 36 | match &db { 37 | Err(e) => { 38 | let db = fresh_db().await?; 39 | return Ok(db); 40 | } 41 | _ => {} 42 | }; 43 | Ok(db.into_diagnostic()?) 44 | } 45 | pub async fn fresh_db() -> Result { 46 | let database_url = "sqlite:////var/spool/jucenit/config.sqlite?mode=rwc"; 47 | let db = sea_orm::Database::connect(database_url) 48 | .await 49 | .into_diagnostic()?; 50 | Migrator::fresh(&db).await.into_diagnostic()?; 51 | Ok(db) 52 | } 53 | #[cfg(test)] 54 | mod test { 55 | use super::*; 56 | // Error Handling 57 | use miette::{IntoDiagnostic, Result}; 58 | 59 | #[tokio::test] 60 | async fn connect_to_db() -> Result<()> { 61 | // connect_db().await?; 62 | fresh_db().await?; 63 | Ok(()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/action.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "action")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | #[sea_orm(unique)] 11 | pub raw_params: String, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation { 16 | #[sea_orm(has_many = "super::ng_match::Entity")] 17 | NgMatch, 18 | } 19 | 20 | impl Related for Entity { 21 | fn to() -> RelationDef { 22 | Relation::NgMatch.def() 23 | } 24 | } 25 | 26 | impl ActiveModelBehavior for ActiveModel {} 27 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/host.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "host")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | #[sea_orm(unique)] 11 | pub domain: String, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation { 16 | #[sea_orm(has_many = "super::match_host::Entity")] 17 | MatchHost, 18 | } 19 | 20 | impl Related for Entity { 21 | fn to() -> RelationDef { 22 | Relation::MatchHost.def() 23 | } 24 | } 25 | 26 | impl Related for Entity { 27 | fn to() -> RelationDef { 28 | super::match_host::Relation::NgMatch.def() 29 | } 30 | fn via() -> Option { 31 | Some(super::match_host::Relation::Host.def().rev()) 32 | } 33 | } 34 | 35 | impl ActiveModelBehavior for ActiveModel {} 36 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/listener.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "listener")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | #[sea_orm(unique)] 11 | pub ip_socket: String, 12 | pub tls: Option, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation { 17 | #[sea_orm(has_many = "super::match_listener::Entity")] 18 | MatchListener, 19 | } 20 | 21 | impl Related for Entity { 22 | fn to() -> RelationDef { 23 | Relation::MatchListener.def() 24 | } 25 | } 26 | 27 | impl Related for Entity { 28 | fn to() -> RelationDef { 29 | super::match_listener::Relation::NgMatch.def() 30 | } 31 | fn via() -> Option { 32 | Some(super::match_listener::Relation::Listener.def().rev()) 33 | } 34 | } 35 | 36 | impl ActiveModelBehavior for ActiveModel {} 37 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/match_action.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "match_action")] 7 | pub struct Model { 8 | #[sea_orm(primary_key, auto_increment = false)] 9 | pub match_id: i32, 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub action_id: i32, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation { 16 | #[sea_orm( 17 | belongs_to = "super::action::Entity", 18 | from = "Column::ActionId", 19 | to = "super::action::Column::Id", 20 | on_update = "Cascade", 21 | on_delete = "SetNull" 22 | )] 23 | Action, 24 | #[sea_orm( 25 | belongs_to = "super::ng_match::Entity", 26 | from = "Column::MatchId", 27 | to = "super::ng_match::Column::Id", 28 | on_update = "Cascade", 29 | on_delete = "SetNull" 30 | )] 31 | NgMatch, 32 | } 33 | 34 | impl Related for Entity { 35 | fn to() -> RelationDef { 36 | Relation::Action.def() 37 | } 38 | } 39 | 40 | impl Related for Entity { 41 | fn to() -> RelationDef { 42 | Relation::NgMatch.def() 43 | } 44 | } 45 | 46 | impl ActiveModelBehavior for ActiveModel {} 47 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/match_host.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "match_host")] 7 | pub struct Model { 8 | #[sea_orm(primary_key, auto_increment = false)] 9 | pub match_id: i32, 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub host_id: i32, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation { 16 | #[sea_orm( 17 | belongs_to = "super::host::Entity", 18 | from = "Column::HostId", 19 | to = "super::host::Column::Id", 20 | on_update = "Cascade", 21 | on_delete = "Cascade" 22 | )] 23 | Host, 24 | #[sea_orm( 25 | belongs_to = "super::ng_match::Entity", 26 | from = "Column::MatchId", 27 | to = "super::ng_match::Column::Id", 28 | on_update = "Cascade", 29 | on_delete = "Cascade" 30 | )] 31 | NgMatch, 32 | } 33 | 34 | impl Related for Entity { 35 | fn to() -> RelationDef { 36 | Relation::Host.def() 37 | } 38 | } 39 | 40 | impl Related for Entity { 41 | fn to() -> RelationDef { 42 | Relation::NgMatch.def() 43 | } 44 | } 45 | 46 | impl ActiveModelBehavior for ActiveModel {} 47 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/match_listener.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "match_listener")] 7 | pub struct Model { 8 | #[sea_orm(primary_key, auto_increment = false)] 9 | pub match_id: i32, 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub listener_id: i32, 12 | } 13 | 14 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 15 | pub enum Relation { 16 | #[sea_orm( 17 | belongs_to = "super::listener::Entity", 18 | from = "Column::ListenerId", 19 | to = "super::listener::Column::Id", 20 | on_update = "Cascade", 21 | on_delete = "Cascade" 22 | )] 23 | Listener, 24 | #[sea_orm( 25 | belongs_to = "super::ng_match::Entity", 26 | from = "Column::MatchId", 27 | to = "super::ng_match::Column::Id", 28 | on_update = "Cascade", 29 | on_delete = "Cascade" 30 | )] 31 | NgMatch, 32 | } 33 | 34 | impl Related for Entity { 35 | fn to() -> RelationDef { 36 | Relation::Listener.def() 37 | } 38 | } 39 | 40 | impl Related for Entity { 41 | fn to() -> RelationDef { 42 | Relation::NgMatch.def() 43 | } 44 | } 45 | 46 | impl ActiveModelBehavior for ActiveModel {} 47 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/mod.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | pub mod prelude; 4 | 5 | pub mod action; 6 | pub mod host; 7 | pub mod listener; 8 | pub mod match_host; 9 | pub mod match_listener; 10 | pub mod ng_match; 11 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/ng_match.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | use sea_orm::entity::prelude::*; 4 | 5 | #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] 6 | #[sea_orm(table_name = "ng_match")] 7 | pub struct Model { 8 | #[sea_orm(primary_key)] 9 | pub id: i32, 10 | #[sea_orm(unique)] 11 | pub uuid: String, 12 | pub action_id: i32, 13 | pub raw_params: Option, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 17 | pub enum Relation { 18 | #[sea_orm( 19 | belongs_to = "super::action::Entity", 20 | from = "Column::ActionId", 21 | to = "super::action::Column::Id", 22 | on_update = "NoAction", 23 | on_delete = "NoAction" 24 | )] 25 | Action, 26 | #[sea_orm(has_many = "super::match_host::Entity")] 27 | MatchHost, 28 | #[sea_orm(has_many = "super::match_listener::Entity")] 29 | MatchListener, 30 | } 31 | 32 | impl Related for Entity { 33 | fn to() -> RelationDef { 34 | Relation::Action.def() 35 | } 36 | } 37 | 38 | impl Related for Entity { 39 | fn to() -> RelationDef { 40 | Relation::MatchHost.def() 41 | } 42 | } 43 | 44 | impl Related for Entity { 45 | fn to() -> RelationDef { 46 | Relation::MatchListener.def() 47 | } 48 | } 49 | 50 | impl Related for Entity { 51 | fn to() -> RelationDef { 52 | super::match_host::Relation::Host.def() 53 | } 54 | fn via() -> Option { 55 | Some(super::match_host::Relation::NgMatch.def().rev()) 56 | } 57 | } 58 | 59 | impl Related for Entity { 60 | fn to() -> RelationDef { 61 | super::match_listener::Relation::Listener.def() 62 | } 63 | fn via() -> Option { 64 | Some(super::match_listener::Relation::NgMatch.def().rev()) 65 | } 66 | } 67 | 68 | impl ActiveModelBehavior for ActiveModel {} 69 | -------------------------------------------------------------------------------- /jucenit_core/src/database/entity/prelude.rs: -------------------------------------------------------------------------------- 1 | //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.15 2 | 3 | pub use super::action::Entity as Action; 4 | pub use super::host::Entity as Host; 5 | pub use super::listener::Entity as Listener; 6 | pub use super::match_host::Entity as MatchHost; 7 | pub use super::match_listener::Entity as MatchListener; 8 | pub use super::ng_match::Entity as NgMatch; 9 | -------------------------------------------------------------------------------- /jucenit_core/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! The main/core struct to be manipulated 3 | //! 4 | //! Jucenit uses a kind of main store struct that eases the generation of 5 | //! an nginx-unit Json configuration. 6 | //! 7 | //! This is a powerful intermediate 8 | //! that is, in the end, lossy converted to a nginx-unit configuration. 9 | //! 10 | mod crud; 11 | pub mod entity; 12 | 13 | // Reexports 14 | // pub use crud::*; 15 | pub use crud::{connect_db, fresh_db}; 16 | pub use entity::*; 17 | -------------------------------------------------------------------------------- /jucenit_core/src/error/mod.rs: -------------------------------------------------------------------------------- 1 | // Tests 2 | // mod test; 3 | 4 | // Error Handling 5 | use miette::{Diagnostic, SourceOffset, SourceSpan}; 6 | use std::{convert, fmt, option}; 7 | use thiserror::Error; 8 | 9 | #[derive(Error, Diagnostic, Debug)] 10 | pub enum CastError { 11 | #[error(transparent)] 12 | #[diagnostic(transparent)] 13 | JsonError(#[from] JsonError), 14 | #[error(transparent)] 15 | #[diagnostic(transparent)] 16 | YamlError(#[from] YamlError), 17 | #[error(transparent)] 18 | #[diagnostic(transparent)] 19 | TomlError(#[from] TomlError), 20 | } 21 | 22 | /** 23 | A JSON report type with hint, colors and code span. 24 | For better configuration file debugging 25 | */ 26 | #[derive(Error, Diagnostic, Debug)] 27 | #[diagnostic(code(cast::json))] 28 | #[error("Serde: Could not convert Json into Rust types")] 29 | pub struct JsonError { 30 | #[source] 31 | pub origin: serde_json::Error, 32 | #[label("here")] 33 | pub at: SourceSpan, 34 | #[source_code] 35 | pub src: String, 36 | } 37 | impl JsonError { 38 | pub fn new(e: serde_json::Error, src: &str) -> Self { 39 | JsonError { 40 | at: SourceSpan::new( 41 | SourceOffset::from_location( 42 | //source 43 | src, 44 | e.line(), 45 | e.column(), 46 | ), 47 | 1, 48 | ), 49 | src: src.to_owned(), 50 | origin: e, 51 | } 52 | } 53 | } 54 | 55 | /** 56 | A YAML report type with hint, colors and code span. 57 | For better configuration file debugging 58 | */ 59 | #[derive(Error, Diagnostic, Debug)] 60 | #[diagnostic(code(cast::yaml))] 61 | #[error("Serde: Could not convert Yaml into Rust types")] 62 | pub struct YamlError { 63 | #[source] 64 | pub origin: serde_yaml::Error, 65 | #[label("here")] 66 | pub at: SourceSpan, 67 | #[source_code] 68 | pub src: String, 69 | } 70 | impl YamlError { 71 | pub fn new(e: serde_yaml::Error, src: &str) -> Self { 72 | if let Some(location) = e.location() { 73 | let line = location.line(); 74 | let column = location.column(); 75 | YamlError { 76 | at: SourceSpan::new( 77 | SourceOffset::from_location( 78 | //source 79 | src, line, column, 80 | ), 81 | 1, 82 | ), 83 | src: src.to_owned(), 84 | origin: e, 85 | } 86 | } else { 87 | YamlError { 88 | at: SourceSpan::new(0.into(), 0), 89 | src: src.to_owned(), 90 | origin: e, 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | A TOML report type with hint, colors and code span. 98 | For better configuration file debugging 99 | */ 100 | #[derive(Error, Diagnostic, Debug)] 101 | #[diagnostic(code(cast::toml))] 102 | #[error("Serde: Could not convert Toml into Rust types")] 103 | pub struct TomlError { 104 | #[source] 105 | pub origin: toml::de::Error, 106 | #[label("here")] 107 | pub at: SourceSpan, 108 | #[source_code] 109 | pub src: String, 110 | } 111 | impl TomlError { 112 | pub fn new(e: toml::de::Error, src: &str) -> Self { 113 | if let Some(span) = e.span() { 114 | let line = span.start; 115 | let column = span.end; 116 | TomlError { 117 | at: SourceSpan::new( 118 | SourceOffset::from_location( 119 | //source 120 | src, line, column, 121 | ), 122 | 1, 123 | ), 124 | src: src.to_owned(), 125 | origin: e, 126 | } 127 | } else { 128 | TomlError { 129 | at: SourceSpan::new(0.into(), 0), 130 | src: src.to_owned(), 131 | origin: e, 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /jucenit_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(debug_assertions, allow(dead_code, unused_imports, unused_variables))] 2 | 3 | mod cast; 4 | pub mod database; 5 | mod error; 6 | pub mod nginx; 7 | mod ssl; 8 | pub use cast::{Action, Config as ConfigFile, Match, Unit as ConfigUnit}; 9 | pub use nginx::{CertificateStore, Config as NginxConfig, Nginx}; 10 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/certificate/crud.rs: -------------------------------------------------------------------------------- 1 | use super::{CertificateInfo, CertificateStore, RawCertificate}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::default::Default; 4 | // Globals 5 | use crate::nginx::SETTINGS; 6 | // Error Handling 7 | use crate::error::JsonError; 8 | use miette::{Error, IntoDiagnostic, Result}; 9 | 10 | use chrono::{DateTime, Duration, FixedOffset, NaiveDateTime, Utc}; 11 | use std::collections::HashMap; 12 | 13 | impl CertificateStore { 14 | /** 15 | * Get a certificate from nginx-unit certificate store. 16 | */ 17 | pub async fn get(dns: &str) -> Result { 18 | let settings = SETTINGS.lock().await.clone(); 19 | 20 | let mut cert = reqwest::get(settings.get_url() + "/certificates/" + dns + "/chain") 21 | .await 22 | .into_diagnostic()? 23 | .json::>() 24 | .await 25 | .into_diagnostic()?; 26 | 27 | // Get first element 28 | let message = format!("No certificate in the store for {:?}", dns); 29 | let err = Error::msg(message); 30 | 31 | cert.reverse(); 32 | cert.pop().ok_or(err) 33 | } 34 | /** 35 | * Get every certificate from nginx-unit certificate store. 36 | */ 37 | pub async fn get_all() -> Result> { 38 | let settings = SETTINGS.lock().await.clone(); 39 | let res = reqwest::get(settings.get_url() + "/certificates") 40 | .await 41 | .into_diagnostic()? 42 | .json::>() 43 | .await 44 | .into_diagnostic()?; 45 | 46 | let mut map: HashMap = HashMap::new(); 47 | for (k, v) in res.iter() { 48 | map.insert(k.to_owned(), v.chain.first().unwrap().to_owned()); 49 | } 50 | Ok(map) 51 | } 52 | /** 53 | * Get every certificate non close to expirity from nginx-unit certificate store. 54 | */ 55 | pub async fn get_all_valid() -> Result> { 56 | let mut res = Self::get_all().await?; 57 | res.retain(|k, v| !v.validity.should_renew().unwrap()); 58 | Ok(res) 59 | } 60 | /** 61 | * Get every almost expired certificate from nginx-unit certificate store. 62 | */ 63 | pub async fn get_all_expired() -> Result> { 64 | let mut res = Self::get_all().await?; 65 | res.retain(|k, v| v.validity.should_renew().unwrap()); 66 | Ok(res) 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use crate::cast::Config as ConfigFile; 73 | use crate::database::{connect_db, fresh_db}; 74 | use crate::nginx::CertificateStore; 75 | use crate::ssl; 76 | use crate::ssl::Fake as FakeCertificate; 77 | use crate::ssl::Letsencrypt as LetsencryptCertificate; 78 | use crate::NginxConfig; 79 | use std::path::PathBuf; 80 | 81 | // Error Handling 82 | use miette::{Error, IntoDiagnostic, Result}; 83 | 84 | /** 85 | * Set a fresh testing environment: 86 | * - clean certificate store 87 | * - set minimal nginx configuration 88 | */ 89 | async fn set_testing_config() -> Result<()> { 90 | CertificateStore::clean().await?; 91 | 92 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 93 | path.push("../examples/jucenit.toml"); 94 | 95 | let config = ConfigFile::load(path.to_str().unwrap())?; 96 | config.set().await?; 97 | 98 | Ok(()) 99 | } 100 | 101 | /** 102 | * Generate a new certificate and upload it to nginx-unit 103 | */ 104 | async fn gen_fake_cert(dns: &str) -> Result<()> { 105 | let bundle = FakeCertificate::get(dns)?; 106 | let _ = CertificateStore::remove(dns).await; 107 | let res = CertificateStore::add(dns, &bundle).await?; 108 | Ok(()) 109 | } 110 | 111 | #[tokio::test] 112 | async fn get_all_certs() -> Result<()> { 113 | let res = CertificateStore::get_all().await?; 114 | println!("{:#?}", res); 115 | Ok(()) 116 | } 117 | 118 | #[tokio::test] 119 | async fn get_validity_info() -> Result<()> { 120 | let dns = "example.com"; 121 | gen_fake_cert(dns).await?; 122 | let cert = CertificateStore::get(&dns).await?; 123 | 124 | println!( 125 | "Certificate remainig vaildity time: {:#?} weeks", 126 | cert.validity.remaining_time()?.num_weeks() 127 | ); 128 | let bool = cert.validity.should_renew()?; 129 | println!("Should be renewed (<=3 weeks)?: {:?}", bool); 130 | Ok(()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/certificate/mappings.rs: -------------------------------------------------------------------------------- 1 | use super::CertificateStore; 2 | use serde::{Deserialize, Serialize}; 3 | use std::default::Default; 4 | // Globals 5 | use crate::nginx::SETTINGS; 6 | // Error Handling 7 | use crate::error::JsonError; 8 | use miette::{Error, IntoDiagnostic, Result}; 9 | 10 | use chrono::{DateTime, Duration, FixedOffset, NaiveDateTime, Utc}; 11 | use std::collections::HashMap; 12 | 13 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 14 | pub struct RawCertificate { 15 | key: String, 16 | pub chain: Vec, 17 | } 18 | 19 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 20 | pub struct CertificateInfo { 21 | subject: Identity, 22 | issuer: Identity, 23 | pub validity: Validity, 24 | } 25 | impl CertificateInfo { 26 | pub fn should_renew(self: &Self) -> Result<()> { 27 | println!("{:?}", self.validity); 28 | Ok(()) 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 33 | pub struct Identity { 34 | common_name: String, 35 | } 36 | 37 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 38 | pub struct Validity { 39 | since: String, 40 | until: String, 41 | } 42 | impl Validity { 43 | pub fn remaining_time(&self) -> Result { 44 | ComputeValidity::from(self).remaining_time() 45 | } 46 | pub fn should_renew(&self) -> Result { 47 | ComputeValidity::from(self).should_renew() 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone, Default)] 52 | pub struct ComputeValidity { 53 | since: DateTime, 54 | until: DateTime, 55 | } 56 | impl From<&Validity> for ComputeValidity { 57 | fn from(e: &Validity) -> ComputeValidity { 58 | ComputeValidity { 59 | since: NaiveDateTime::parse_from_str(&e.since, "%b %e %T %Y %Z") 60 | .unwrap() 61 | .and_utc(), 62 | until: NaiveDateTime::parse_from_str(&e.until, "%b %e %T %Y %Z") 63 | .unwrap() 64 | .and_utc(), 65 | } 66 | } 67 | } 68 | impl ComputeValidity { 69 | pub fn remaining_time(&self) -> Result { 70 | let rest = self.until - Utc::now(); 71 | Ok(rest) 72 | } 73 | pub fn should_renew(&self) -> Result { 74 | Ok(self.remaining_time()? <= Duration::weeks(3)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/certificate/methods.rs: -------------------------------------------------------------------------------- 1 | // Error Handling 2 | use miette::{Error, IntoDiagnostic, Result}; 3 | // Ssl utils 4 | use crate::ssl; 5 | use crate::ssl::Letsencrypt as LetsencryptCertificate; 6 | use crate::ConfigFile; 7 | use rayon::prelude::*; 8 | use std::collections::HashMap; 9 | 10 | // Globals 11 | use crate::nginx::Config as NginxConfig; 12 | use crate::nginx::SETTINGS; 13 | 14 | use crate::database::connect_db; 15 | use crate::database::entity::{prelude::*, *}; 16 | use sea_orm::{prelude::*, query::*, sea_query::OnConflict, ActiveValue, InsertResult}; 17 | 18 | // Loop 19 | use futures::future::{join_all, try_join_all}; 20 | use futures::Future; 21 | use std::thread::sleep; 22 | use std::time::{Duration, *}; 23 | use tokio::task::JoinSet; 24 | 25 | // Struct 26 | use super::CertificateInfo; 27 | 28 | #[derive(Debug, Clone, Default)] 29 | pub struct CertificateStore; 30 | impl CertificateStore { 31 | /** 32 | * Poll the configuration for hosts and seek through certificate store 33 | * for matching valid certificates or generate them, 34 | * and update nginx-unit configuration with fresh ssl. 35 | */ 36 | pub async fn hydrate() -> Result<()> { 37 | let db = connect_db().await?; 38 | let hosts = Host::find().all(&db).await.into_diagnostic()?; 39 | let domains: Vec = hosts.iter().map(|x| x.domain.clone()).collect(); 40 | 41 | let parallel = domains.iter().map(Self::hydrate_one); 42 | // TODO 43 | // Use JoinSet instead of join_all for better error report 44 | try_join_all(parallel).await?; 45 | 46 | // Update listeners tls option with fresh certs 47 | // By updating whole config 48 | let config = ConfigFile::pull().await?; 49 | config.push().await?; 50 | 51 | // Clean failed challenge routes 52 | ConfigFile::purge_http_challenge().await?; 53 | 54 | Ok(()) 55 | } 56 | 57 | async fn hydrate_one(host: &String) -> Result<()> { 58 | let dns = host.to_owned(); 59 | // For ACME limitation rate reason 60 | // Check if a certificate already exists 61 | let cert = CertificateStore::get(&dns).await; 62 | match cert { 63 | Ok(res) => { 64 | if res.validity.should_renew()? { 65 | CertificateStore::descrete_update(&dns).await?; 66 | } 67 | } 68 | Err(_) => { 69 | CertificateStore::descrete_update(&dns).await?; 70 | } 71 | } 72 | Ok(()) 73 | } 74 | /** 75 | * Replace a certificate bundle: 76 | * - a .pem file 77 | * with intermediate certs and private key) 78 | * to nginx-unit certificate store 79 | * 80 | * Fail silently. 81 | */ 82 | pub async fn descrete_update(dns: &str) -> Result<()> { 83 | // Remove preceding certificate if it exists 84 | let res = CertificateStore::update(&dns).await; 85 | match res { 86 | Ok(res) => {} 87 | Err(e) => { 88 | println!("{}", e); 89 | } 90 | }; 91 | Ok(()) 92 | } 93 | /** 94 | * Replace a certificate bundle: 95 | * - a .pem file 96 | * with intermediate certs and private key) 97 | * to nginx-unit certificate store 98 | */ 99 | pub async fn update(dns: &str) -> Result { 100 | let account = ssl::set_account().await?.clone(); 101 | let bundle = LetsencryptCertificate::get_cert_bundle(&dns, &account).await?; 102 | // Remove preceding certificate if it exists 103 | let _ = CertificateStore::remove(dns).await; 104 | 105 | let res = CertificateStore::add(dns, &bundle).await?; 106 | Ok(res) 107 | } 108 | /** 109 | * Poll certificate store and declared hosts every minutes for changes. 110 | */ 111 | pub async fn watch() -> Result<()> { 112 | loop { 113 | CertificateStore::hydrate().await?; 114 | sleep(Duration::from_secs(60)); 115 | } 116 | } 117 | /** 118 | * Remove every certificate from nginx-unit certificate store. 119 | * and update nginx-unit configuration 120 | */ 121 | pub async fn clean() -> Result<()> { 122 | // Get list of every certificates in nginx-unit certificate store. 123 | let certificates = CertificateStore::get_all().await?; 124 | for (key, _) in certificates { 125 | CertificateStore::remove(&key).await?; 126 | } 127 | // Update routes 128 | Ok(()) 129 | } 130 | /** 131 | * Upload a certificate bundle: 132 | * - a .pem file 133 | * with intermediate certs and private key) 134 | * to nginx-unit certificate store 135 | */ 136 | pub async fn add(dns: &str, bundle: &str) -> Result { 137 | let settings = SETTINGS.lock().await.clone(); 138 | let client = reqwest::Client::new(); 139 | let res = client 140 | .put(settings.get_url() + "/certificates/" + dns) 141 | .body(bundle.to_owned()) 142 | .send() 143 | .await 144 | .into_diagnostic()? 145 | .json::() 146 | .await 147 | .into_diagnostic()?; 148 | Ok(res) 149 | } 150 | /** 151 | * Remove a certificate from nginx-unit certificate store. 152 | */ 153 | pub async fn remove(dns: &str) -> Result { 154 | let settings = SETTINGS.lock().await.clone(); 155 | let client = reqwest::Client::new(); 156 | let res = client 157 | .delete(settings.get_url() + "/certificates/" + &dns) 158 | .send() 159 | .await 160 | .into_diagnostic()? 161 | .json::() 162 | .await 163 | .into_diagnostic()?; 164 | Ok(res) 165 | } 166 | } 167 | 168 | #[cfg(test)] 169 | mod tests { 170 | 171 | use super::CertificateStore; 172 | use crate::ssl; 173 | use crate::ssl::Fake as FakeCertificate; 174 | use crate::ssl::Letsencrypt as LetsencryptCertificate; 175 | use std::path::PathBuf; 176 | 177 | use miette::Result; 178 | 179 | use crate::ConfigFile; 180 | use crate::NginxConfig; 181 | 182 | use serial_test::serial; 183 | 184 | /** 185 | * Set a fresh testing environment: 186 | * - clean certificate store 187 | * - set minimal nginx configuration 188 | */ 189 | async fn set_testing_config() -> Result<()> { 190 | CertificateStore::clean().await?; 191 | 192 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 193 | path.push("../examples/jucenit.toml"); 194 | 195 | let config = ConfigFile::load(path.to_str().unwrap())?; 196 | config.set().await?; 197 | 198 | Ok(()) 199 | } 200 | 201 | // #[tokio::test] 202 | // #[serial] 203 | async fn clean_cert_store() -> Result<()> { 204 | let res = CertificateStore::clean().await?; 205 | println!("{:#?}", res); 206 | Ok(()) 207 | } 208 | // #[tokio::test] 209 | // #[serial] 210 | async fn remove_cert() -> Result<()> { 211 | let dns = "example.com"; 212 | let res = CertificateStore::remove(dns).await?; 213 | println!("{:#?}", res); 214 | Ok(()) 215 | } 216 | // #[tokio::test] 217 | // #[serial] 218 | async fn add_fake_cert() -> Result<()> { 219 | set_testing_config().await?; 220 | let dns = "example.com"; 221 | let bundle = FakeCertificate::get(dns)?; 222 | let res = CertificateStore::add(dns, &bundle).await?; 223 | println!("{:#?}", res); 224 | Ok(()) 225 | } 226 | // #[tokio::test] 227 | // #[serial] 228 | async fn update_cert_letsencrypt() -> Result<()> { 229 | set_testing_config().await?; 230 | let dns = "example.com"; 231 | let res = CertificateStore::update(dns).await?; 232 | println!("{:#?}", res); 233 | Ok(()) 234 | } 235 | 236 | #[tokio::test] 237 | #[serial] 238 | async fn hydrate_cert_store() -> Result<()> { 239 | set_testing_config().await?; 240 | 241 | let res = CertificateStore::hydrate().await?; 242 | 243 | let certificates = CertificateStore::get_all().await?; 244 | let mut dns_list: Vec = certificates.into_keys().collect(); 245 | dns_list.sort(); 246 | let mut expected = vec![ 247 | "api.example.com".to_owned(), 248 | "example.com".to_owned(), 249 | "test.com".to_owned(), 250 | ]; 251 | expected.sort(); 252 | 253 | assert_eq!(expected, dns_list); 254 | Ok(()) 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/certificate/mod.rs: -------------------------------------------------------------------------------- 1 | mod crud; 2 | mod mappings; 3 | mod methods; 4 | 5 | // Reexports 6 | pub use crud::*; 7 | pub use mappings::*; 8 | pub use methods::*; 9 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/config/crud.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use serde::{Deserialize, Serialize}; 3 | use std::env::temp_dir; 4 | use tokio::task::spawn_local; 5 | // Global vars 6 | use crate::nginx::SETTINGS; 7 | // Error Handling 8 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 9 | // exec 10 | use crate::nginx::certificate::CertificateStore; 11 | use crate::ssl::Fake as FakeCertificate; 12 | use crate::ssl::Letsencrypt as LetsencryptCertificate; 13 | use crate::{ssl, Nginx}; 14 | // Async 15 | use futures::executor::block_on; 16 | 17 | // Config file 18 | use crate::cast::Config as ConfigFile; 19 | 20 | use http::uri::Uri; 21 | use std::env; 22 | 23 | // Common structs to file config and unit config 24 | #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] 25 | #[serde(deny_unknown_fields)] 26 | pub struct Action { 27 | #[serde(skip_serializing_if = "Option::is_none")] 28 | #[serde(flatten)] 29 | pub raw_params: Option, 30 | } 31 | 32 | #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] 33 | #[serde(deny_unknown_fields)] 34 | pub struct Match { 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub host: Option, 37 | 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | #[serde(flatten)] 40 | pub raw_params: Option, 41 | } 42 | 43 | #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] 44 | #[serde(deny_unknown_fields)] 45 | pub struct ListenerOpts { 46 | pub pass: String, 47 | #[serde(skip_serializing_if = "Option::is_none")] 48 | pub tls: Option, 49 | } 50 | 51 | #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] 52 | #[serde(deny_unknown_fields)] 53 | pub struct Tls { 54 | pub certificate: Vec, 55 | } 56 | 57 | #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] 58 | pub struct Route { 59 | pub action: Option, 60 | #[serde(rename = "match")] 61 | pub match_: Match, 62 | } 63 | 64 | #[derive(Debug, Serialize, Deserialize, Clone)] 65 | pub struct Config { 66 | pub listeners: IndexMap, 67 | pub routes: IndexMap>, 68 | pub settings: Option, 69 | pub access_log: Option, 70 | } 71 | impl Default for Config { 72 | fn default() -> Self { 73 | let settings = Some(serde_json::json!({ 74 | "http": { 75 | "log_route": true 76 | } 77 | })); 78 | let access_log = Some(serde_json::json!({ 79 | "path": "/var/log/unit/access.log" 80 | })); 81 | 82 | let listeners = IndexMap::new(); 83 | 84 | // Ensure routes is an array 85 | // Avoid Json inconsistency 86 | let routes = IndexMap::new(); 87 | Config { 88 | routes, 89 | listeners, 90 | settings, 91 | access_log, 92 | } 93 | } 94 | } 95 | 96 | impl Config { 97 | /** 98 | * Replace the in place configuration. 99 | */ 100 | pub async fn set(&self) -> Result { 101 | let settings = SETTINGS.lock().await.clone(); 102 | let client = reqwest::Client::new(); 103 | let res = client 104 | .put(settings.get_url() + "/config") 105 | .body(serde_json::to_string(&self).into_diagnostic()?) 106 | .send() 107 | .await 108 | .into_diagnostic()? 109 | .json::() 110 | .await 111 | .into_diagnostic()?; 112 | 113 | // Response conversion from Json to Rust type. 114 | match res { 115 | serde_json::Value::Object(res) => { 116 | if let Some(success) = res.get("success") { 117 | println!("nginx-server: {}", success); 118 | } else if let Some(error) = res.get("error") { 119 | return Err(Error::msg(error.to_string())); 120 | } else { 121 | let message = format!( 122 | "Unexpected error returned from nginx-server:\n 123 | {:#?}", 124 | res 125 | ); 126 | return Err(Error::msg(message)); 127 | } 128 | } 129 | _ => { 130 | let message = format!( 131 | "Unexpected value returned from nginx-server:\n 132 | {}", 133 | res 134 | ); 135 | return Err(Error::msg(message)); 136 | } 137 | }; 138 | return Ok(self.clone()); 139 | } 140 | 141 | /** 142 | * Get the nginx-unit configuration as a rust struct. 143 | */ 144 | pub async fn get() -> Result { 145 | let settings = SETTINGS.lock().await.clone(); 146 | let config = reqwest::get(settings.get_url() + "/config") 147 | .await 148 | .into_diagnostic()? 149 | .json::() 150 | .await 151 | .into_diagnostic()?; 152 | Ok(config) 153 | } 154 | } 155 | 156 | #[cfg(test)] 157 | mod tests { 158 | 159 | use crate::cast::Config as ConfigFile; 160 | use crate::nginx::{Config as NginxConfig, Nginx}; 161 | use std::path::PathBuf; 162 | // Error handling 163 | use miette::{IntoDiagnostic, Result}; 164 | 165 | #[tokio::test] 166 | async fn get_config() -> Result<()> { 167 | let res = NginxConfig::get().await?; 168 | println!("{:#?}", res); 169 | Ok(()) 170 | } 171 | 172 | #[tokio::test] 173 | async fn set_default_config() -> Result<()> { 174 | let res = NginxConfig::set(&NginxConfig::default()).await?; 175 | println!("{:#?}", res); 176 | Ok(()) 177 | } 178 | 179 | #[tokio::test] 180 | async fn set_config_from_file() -> Result<()> { 181 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 182 | path.push("../examples/jucenit.toml"); 183 | 184 | let config = ConfigFile::load(path.to_str().unwrap())?; 185 | config.set().await?; 186 | 187 | let nginx = NginxConfig::pull().await?; 188 | 189 | let json = serde_json::to_string_pretty(&nginx).into_diagnostic()?; 190 | println!("{}", json); 191 | 192 | let res = nginx.set().await?; 193 | println!("{:#?}", res); 194 | 195 | Ok(()) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/config/from.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | nginx::config::crud::{Action, ListenerOpts, Match, Tls}, 3 | CertificateStore, ConfigFile, ConfigUnit, 4 | }; 5 | // Database / Sea orm 6 | // use indexmap::IndexMap; 7 | use crate::database::entity::{prelude::*, *}; 8 | use indexmap::IndexMap; 9 | use migration::{Migrator, MigratorTrait}; 10 | use sea_orm::{prelude::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase}; 11 | use sea_orm::{Database, DatabaseConnection}; 12 | // Logging 13 | use tracing::{debug, Level}; 14 | // Error Handling 15 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 16 | use uuid::{uuid, Uuid}; 17 | 18 | use super::Route; 19 | 20 | // impl From<&entity::prelude::Listener> for ListenerOpts { 21 | impl ListenerOpts { 22 | pub async fn from(e: &listener::Model) -> Result<(String, ListenerOpts)> { 23 | // Bulk add certificates to listeners 24 | let certs: Vec = CertificateStore::get_all_valid() 25 | .await? 26 | .into_keys() 27 | .into_iter() 28 | .collect(); 29 | 30 | let tls: Option; 31 | if certs.is_empty() || e.ip_socket.ends_with(":80") { 32 | tls = None 33 | } else { 34 | tls = Some(Tls { certificate: certs }) 35 | } 36 | 37 | let tuples = ( 38 | e.ip_socket.to_owned(), 39 | ListenerOpts { 40 | pass: format!("routes/jucenit_[{}]", e.ip_socket), 41 | tls, 42 | }, 43 | ); 44 | Ok(tuples) 45 | } 46 | } 47 | 48 | impl Match { 49 | pub fn from(e: &ng_match::Model, h: Option) -> Match { 50 | let mut host: Option = None; 51 | if let Some(h) = h { 52 | host = Some(h.domain); 53 | } 54 | let match_ = Match { 55 | host, 56 | raw_params: e 57 | .raw_params 58 | .clone() 59 | .map(|x| serde_json::from_str(&x).unwrap()), 60 | }; 61 | match_ 62 | } 63 | } 64 | impl Action { 65 | pub fn from(e: &action::Model) -> Action { 66 | let action = Action { 67 | raw_params: serde_json::from_str(&e.raw_params).unwrap(), 68 | }; 69 | action 70 | } 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use crate::{ 76 | nginx::config::crud::{Action, ListenerOpts, Match}, 77 | ConfigFile, ConfigUnit, 78 | }; 79 | use serde_json::json; 80 | use uuid::{uuid, Uuid}; 81 | // SeaOrm 82 | use crate::database::entity::{prelude::*, *}; 83 | use sea_orm::{prelude::*, query::*, ActiveValue, TryIntoModel}; 84 | // Error Handling 85 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 86 | 87 | #[tokio::test] 88 | async fn convert_listener() -> Result<()> { 89 | let listener = listener::Model { 90 | id: 4, 91 | ip_socket: "*:8082".to_owned(), 92 | tls: None, 93 | }; 94 | let expect = ( 95 | "*:8082".to_owned(), 96 | ListenerOpts { 97 | pass: "routes/jucenit_[*:8082]".to_owned(), 98 | tls: None, 99 | }, 100 | ); 101 | let res = ListenerOpts::from(&listener).await?; 102 | assert_eq!(expect, res); 103 | Ok(()) 104 | } 105 | #[test] 106 | fn convert_match() -> Result<()> { 107 | let host = host::ActiveModel { 108 | id: ActiveValue::Set(9), 109 | domain: ActiveValue::Set("example.com".to_owned()), 110 | }; 111 | let action = action::ActiveModel { 112 | id: ActiveValue::Set(2), 113 | // raw_params: ActiveValue::Set("{}".to_owned()), 114 | ..Default::default() 115 | }; 116 | let match_ = ng_match::ActiveModel { 117 | id: ActiveValue::Set(7), 118 | uuid: ActiveValue::Set(Uuid::new_v4().to_string()), 119 | raw_params: ActiveValue::Set(None), 120 | // raw_params: ActiveValue::Set("{}".to_owned()), 121 | action_id: ActiveValue::Set(2), 122 | }; 123 | 124 | let expect = Match { 125 | host: Some("example.com".to_owned()), 126 | raw_params: None, 127 | }; 128 | let res = Match::from( 129 | &match_.try_into_model().into_diagnostic()?, 130 | Some(host.try_into_model().into_diagnostic()?), 131 | ); 132 | assert_eq!(expect, res); 133 | Ok(()) 134 | } 135 | #[test] 136 | fn convert_action() -> Result<()> { 137 | let action = action::ActiveModel { 138 | id: ActiveValue::Set(2), 139 | raw_params: ActiveValue::Set(json!("{}").to_string()), 140 | ..Default::default() 141 | }; 142 | let expect = Action { 143 | raw_params: Some(json!("{}")), 144 | }; 145 | let res = Action::from(&action.try_into_model().into_diagnostic()?); 146 | assert_eq!(expect, res); 147 | Ok(()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/config/mod.rs: -------------------------------------------------------------------------------- 1 | mod crud; 2 | mod from; 3 | 4 | // Reexports 5 | pub use crud::*; 6 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/from_database.rs: -------------------------------------------------------------------------------- 1 | use crate::nginx::config::{Action, ListenerOpts, Match, Route}; 2 | // Database 3 | use crate::{ConfigFile, ConfigUnit, NginxConfig}; 4 | // Sea orm 5 | // use indexmap::IndexMap; 6 | use crate::database::connect_db; 7 | use crate::database::entity::{prelude::*, *}; 8 | use migration::{MatchHost, MatchListener, Migrator, MigratorTrait}; 9 | use sea_orm::{ 10 | prelude::*, query::*, sea_query::OnConflict, ActiveValue, InsertResult, MockDatabase, 11 | }; 12 | use sea_orm::{Database, DatabaseConnection}; 13 | // Logging 14 | use tracing::{debug, Level}; 15 | // Error Handling 16 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 17 | 18 | impl NginxConfig { 19 | /** 20 | * Generate an nginx unit configuration 21 | * from what has been pushed to the database ever since 22 | */ 23 | pub async fn pull() -> Result { 24 | let db = connect_db().await?; 25 | let mut nginx_config = NginxConfig::default(); 26 | 27 | // Select related listeners and match 28 | // And add them to config struct 29 | let listeners: Vec<(listener::Model, Vec)> = Listener::find() 30 | .find_with_related(NgMatch) 31 | .all(&db) 32 | .await 33 | .into_diagnostic()?; 34 | for (listener, matches) in listeners { 35 | // Append listeners and routes to nginx configuration 36 | let (ip_socket, listener) = ListenerOpts::from(&listener).await?; 37 | nginx_config 38 | .listeners 39 | .insert(ip_socket.clone(), listener.clone()); 40 | let route_name = format!("jucenit_[{}]", ip_socket); 41 | nginx_config.routes.insert(route_name.clone(), vec![]); 42 | 43 | // Select related match and hosts 44 | let matches: Vec<(ng_match::Model, Vec)> = NgMatch::find() 45 | .find_with_related(Host) 46 | .filter( 47 | Condition::all().add(ng_match::Column::Id.is_in(matches.iter().map(|x| x.id))), 48 | ) 49 | .all(&db) 50 | .await 51 | .into_diagnostic()?; 52 | 53 | for (match_, hosts) in &matches { 54 | let action = match_ 55 | .find_related(Action) 56 | .one(&db) 57 | .await 58 | .into_diagnostic()?; 59 | // Convert to nginx struct 60 | let action = action.clone().map(|x| Action::from(&x)); 61 | let route_name = format!("jucenit_[{}]", ip_socket); 62 | let route = nginx_config.routes.get_mut(&route_name); 63 | let route = route.unwrap(); 64 | 65 | if hosts.is_empty() { 66 | route.push(Route { 67 | action: action.clone(), 68 | match_: Match::from(&match_, None), 69 | }); 70 | } else { 71 | for host in hosts { 72 | route.push(Route { 73 | action: action.clone(), 74 | match_: Match::from(&match_, Some(host.to_owned())), 75 | }); 76 | } 77 | } 78 | } 79 | } 80 | Ok(nginx_config) 81 | } 82 | } 83 | 84 | #[cfg(test)] 85 | mod test { 86 | 87 | use super::*; 88 | // Error Handling 89 | use miette::{Error, IntoDiagnostic, Result, WrapErr}; 90 | use std::path::PathBuf; 91 | 92 | /** 93 | * Set a fresh testing environment: 94 | * - clean certificate store 95 | * - set minimal nginx configuration 96 | */ 97 | async fn set_testing_config() -> Result<()> { 98 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 99 | path.push("../examples/jucenit.toml"); 100 | 101 | let config = ConfigFile::load(path.to_str().unwrap())?; 102 | config.set().await?; 103 | 104 | Ok(()) 105 | } 106 | 107 | #[tokio::test] 108 | async fn convert() -> Result<()> { 109 | set_testing_config().await?; 110 | let nginx_config = NginxConfig::pull().await?; 111 | println!("{:#?}", nginx_config); 112 | Ok(()) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod certificate; 2 | pub mod config; 3 | pub mod from_database; 4 | pub mod options; 5 | 6 | // Reexports 7 | pub use certificate::CertificateStore; 8 | pub use config::Config; 9 | pub use from_database::*; 10 | pub use options::{Nginx, SETTINGS}; 11 | -------------------------------------------------------------------------------- /jucenit_core/src/nginx/options.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::default::Default; 3 | // Global vars 4 | use once_cell::sync::Lazy; 5 | use std::sync::Arc; 6 | use tokio::sync::Mutex; 7 | // Error Handling 8 | use miette::{Error, IntoDiagnostic, Result}; 9 | // Structs 10 | use super::{CertificateStore, Config}; 11 | 12 | pub static SETTINGS: Lazy>> = 13 | Lazy::new(|| Arc::new(Mutex::new(Settings::default()))); 14 | 15 | /* 16 | * A struct to query the good nginx-unit socket or port. 17 | */ 18 | #[derive(Debug, Serialize, Deserialize, Clone)] 19 | pub struct Settings { 20 | pub url: Option, 21 | pub socket: Option, 22 | pub state_dir: Option, 23 | } 24 | impl Default for Settings { 25 | fn default() -> Self { 26 | Settings { 27 | url: Some("http://127.0.0.1:8080".to_string()), 28 | socket: None, 29 | state_dir: Some("/var/spool/unit".to_string()), 30 | } 31 | } 32 | } 33 | impl Settings { 34 | pub fn get_url(&self) -> String { 35 | if let Some(url) = &self.url { 36 | return url.to_owned(); 37 | } else if let Some(socket) = &self.socket { 38 | return socket.to_owned(); 39 | } else { 40 | return Settings::default().url.unwrap(); 41 | } 42 | } 43 | } 44 | 45 | // Unit identical structs 46 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 47 | #[serde(deny_unknown_fields)] 48 | pub struct Nginx { 49 | pub config: Config, 50 | pub certificates: serde_json::Value, 51 | pub status: serde_json::Value, 52 | #[serde(skip)] 53 | pub settings: Settings, 54 | } 55 | -------------------------------------------------------------------------------- /jucenit_core/src/ssl/fake.rs: -------------------------------------------------------------------------------- 1 | // File manipulation 2 | use std::fs; 3 | use std::io::Write; 4 | use uuid::Uuid; 5 | // Error Handling 6 | use miette::{Error, IntoDiagnostic, Result}; 7 | // Certificate generation 8 | 9 | use rcgen::{generate_simple_self_signed, CertifiedKey}; 10 | 11 | // Global vars 12 | use crate::nginx::SETTINGS; 13 | 14 | #[derive(Debug, Clone, Default)] 15 | pub struct Fake; 16 | impl Fake { 17 | pub fn get(dns: &str) -> Result { 18 | let names = vec![dns.to_owned()]; 19 | let CertifiedKey { cert, key_pair } = 20 | generate_simple_self_signed(names).into_diagnostic()?; 21 | let bundle = format!("{}{}", cert.pem(), key_pair.serialize_pem()); 22 | Ok(bundle) 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::Fake; 29 | use miette::Result; 30 | 31 | #[test] 32 | fn get_dummy_bundle() -> Result<()> { 33 | let res = Fake::get("example.com")?; 34 | println!("{}", res); 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /jucenit_core/src/ssl/letsencrypt.rs: -------------------------------------------------------------------------------- 1 | use acme2::{gen_rsa_private_key, Authorization}; 2 | use acme2::{ 3 | Account, AccountBuilder, AuthorizationStatus, Challenge, ChallengeStatus, Csr, Directory, 4 | DirectoryBuilder, OrderBuilder, OrderStatus, 5 | }; 6 | use serde_json::json; 7 | use std::time::Duration; 8 | // Error Handling 9 | use miette::{ensure, Context, Error, IntoDiagnostic, Result}; 10 | // use acme2::Error; 11 | // Global vars 12 | use crate::nginx::SETTINGS; 13 | use once_cell::sync::Lazy; 14 | use std::sync::Arc; 15 | use tokio::sync::Mutex; 16 | 17 | // File manipulation 18 | use tokio::fs; 19 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 20 | 21 | use std::path::{Path, PathBuf}; 22 | use toml::toml; 23 | use uuid::Uuid; 24 | // Crate structs 25 | use super::pebble::*; 26 | use crate::{Action, ConfigUnit, Match}; 27 | use openssl::{pkey::PKey, x509::X509}; 28 | 29 | // Production url 30 | pub static LETS_ENCRYPT_URL: Lazy>> = Lazy::new(|| { 31 | Arc::new(Mutex::new( 32 | "https://acme-v02.api.letsencrypt.org/directory".to_owned(), 33 | )) 34 | }); 35 | // Stagging url 36 | // pub static LETS_ENCRYPT_URL: Lazy>> = Lazy::new(|| { 37 | // Arc::new(Mutex::new( 38 | // "https://acme-staging-v02.api.letsencrypt.org/directory".to_owned(), 39 | // )) 40 | // }); 41 | static HTTP_PORT: i32 = 80; 42 | static TLS_PORT: i32 = 443; 43 | 44 | /** 45 | * Create a new ACMEv2 directory for Let's Encrypt. 46 | */ 47 | async fn letsencrypt_directory() -> Result> { 48 | let dir = DirectoryBuilder::new(LETS_ENCRYPT_URL.lock().await.clone()) 49 | .build() 50 | .await 51 | .into_diagnostic()?; 52 | Ok(dir) 53 | } 54 | /** 55 | * Create an ACME account to use for the order. For production 56 | * purposes, you should keep the account (and private key), so 57 | * you can renew your certificate easily. 58 | */ 59 | pub async fn set_account() -> Result> { 60 | // Set a Private key path 61 | let file_path = "/var/spool/jucenit/ssl_account_private_key.pem".to_owned(); 62 | let path = Path::new(&file_path); 63 | 64 | #[cfg(debug_assertions)] 65 | let dir = pebble_directory().await?; 66 | #[cfg(not(debug_assertions))] 67 | let dir = letsencrypt_directory().await?; 68 | 69 | // Retrieve previous account 70 | if path.exists() { 71 | let message = format!("Couldn't open file at: {:?}", file_path); 72 | let data = fs::read(file_path.clone()).await.into_diagnostic()?; 73 | let pkey = PKey::private_key_from_pem(&data).into_diagnostic()?; 74 | 75 | let mut builder = AccountBuilder::new(dir.clone()); 76 | let account = builder 77 | .private_key(pkey) 78 | .contact(vec!["mailto:areskul@areskul.com".to_string()]) 79 | .terms_of_service_agreed(true) 80 | .only_return_existing(true) 81 | .build() 82 | .await 83 | .into_diagnostic()?; 84 | 85 | Ok(account) 86 | 87 | // Create new account 88 | } else { 89 | let mut builder = AccountBuilder::new(dir.clone()); 90 | let account = builder 91 | .contact(vec!["mailto:areskul@areskul.com".to_string()]) 92 | .terms_of_service_agreed(true) 93 | .build() 94 | .await 95 | .into_diagnostic()?; 96 | 97 | let pkey = account.private_key(); 98 | let private_key = pkey.private_key_to_pem_pkcs8().into_diagnostic()?; 99 | 100 | // Write private key to file 101 | let tmp_dir = "/var/spool/jucenit"; 102 | let message = format!("Couldn't create dir: {:?}", tmp_dir); 103 | fs::create_dir_all(tmp_dir) 104 | .await 105 | .into_diagnostic() 106 | .wrap_err(message)?; 107 | 108 | let message = format!("Couldn't create file at: {:?}", file_path); 109 | let mut file = fs::File::create(file_path.clone()) 110 | .await 111 | .into_diagnostic() 112 | .wrap_err(message)?; 113 | file.write_all(&private_key).await.into_diagnostic()?; 114 | 115 | Ok(account) 116 | } 117 | } 118 | 119 | /** 120 | * Create a self-signed certificate to serve domain and resolve challenge. 121 | */ 122 | fn make_jucenit_tls_alpn_challenge_config(dns: &str, challenge: &Challenge) -> Result { 123 | // Challenge ports 124 | let toml = format!( 125 | " 126 | uuid = '{}' 127 | listeners = ['*:{}'] 128 | [match] 129 | hosts = ['{}'] 130 | uri = '/.well-known/acme-challenge/{}' 131 | [action] 132 | share = ['/tmp/jucenit/challenge_{}.txt'] 133 | ", 134 | Uuid::new_v4(), 135 | TLS_PORT, 136 | dns, 137 | challenge.token.clone().unwrap(), 138 | dns 139 | ); 140 | let unit = ConfigUnit::from_toml_str(&toml)?; 141 | let cert = String::new(); 142 | 143 | // todo!(); 144 | 145 | Ok(cert) 146 | } 147 | /** 148 | * Create an nginx-unit match to serve the challenge key file. 149 | * A file at `https://example.com/.well-known/${challenge.token}` 150 | * with the content of `challenge.key_authorization()??`. 151 | */ 152 | async fn make_jucenit_http_challenge_config( 153 | dns: &str, 154 | challenge: &Challenge, 155 | ) -> Result { 156 | // Challenge ports 157 | let http_port = 80; 158 | let tls_port = 443; 159 | 160 | // Update nginx-unit config 161 | let toml = format!( 162 | " 163 | uuid = '{}' 164 | listeners = ['*:{}'] 165 | [match] 166 | hosts = ['{}'] 167 | uri = ['/.well-known/acme-challenge/{}'] 168 | [action] 169 | share = ['/tmp/jucenit/challenge_{}.txt'] 170 | ", 171 | Uuid::new_v4(), 172 | HTTP_PORT, 173 | dns, 174 | challenge.token.clone().unwrap(), 175 | dns 176 | ); 177 | let unit = ConfigUnit::from_toml_str(&toml)?; 178 | 179 | Ok(unit) 180 | } 181 | 182 | /** 183 | * Create tmp challenge files and nginx-unit routes 184 | */ 185 | async fn set_challenge_key_file(dns: &str, challenge: &Challenge) -> Result<()> { 186 | // Write challenge key to temporary file 187 | let data = challenge.key_authorization().into_diagnostic()?.unwrap(); 188 | 189 | // Create and write to file 190 | let tmp_dir = "/tmp/jucenit"; 191 | let message = format!("Couldn't create dir: {:?}", tmp_dir); 192 | fs::create_dir_all(tmp_dir) 193 | .await 194 | .into_diagnostic() 195 | .wrap_err(message)?; 196 | 197 | let file_path = format!("/tmp/jucenit/challenge_{}.txt", dns); 198 | let message = format!("Couldn't create file at: {:?}", file_path); 199 | let mut file = fs::File::create(file_path.clone()) 200 | .await 201 | .into_diagnostic() 202 | .wrap_err(message)?; 203 | let bytes = data.as_bytes(); 204 | file.write_all(bytes).await.into_diagnostic()?; 205 | 206 | Ok(()) 207 | } 208 | 209 | /** 210 | * Delete tmp challenge files and nginx-unit routes 211 | */ 212 | async fn del_challenge_key_file(dns: &str, challenge: &Challenge) -> Result<()> { 213 | let tmp_dir = "/tmp/jucenit"; 214 | let path = format!("{}/challenge_{}.txt", tmp_dir, dns); 215 | fs::remove_file(path).await.into_diagnostic()?; 216 | Ok(()) 217 | } 218 | 219 | #[derive(Debug, Clone, Default)] 220 | pub struct Letsencrypt; 221 | 222 | impl Letsencrypt { 223 | pub async fn get_cert_bundle(dns: &str, account: &Arc) -> Result { 224 | // Create a new order for a specific domain name. 225 | let mut builder = OrderBuilder::new(account.to_owned()); 226 | builder.add_dns_identifier(dns.to_owned()); 227 | let order = builder.build().await.into_diagnostic()?; 228 | 229 | // Get the list of needed authorizations for this order. 230 | let authorizations = order.authorizations().await.into_diagnostic()?; 231 | for auth in authorizations { 232 | // Get an tls-alpn-01 challenge 233 | // if let Some(challenge) = auth.get_challenge("tls-alpn-01") { 234 | // Self::tls_alpn_challenge(dns, auth, &challenge).await?; 235 | // } 236 | // Get an http-01 challenge 237 | if let Some(challenge) = auth.get_challenge("http-01") { 238 | Self::http_challenge(dns, auth, &challenge).await?; 239 | } 240 | } 241 | 242 | let order = order 243 | .wait_ready(Duration::from_secs(5), 10) 244 | .await 245 | .into_diagnostic()?; 246 | ensure!(OrderStatus::Ready == order.status, "Order no Ready"); 247 | 248 | // Generate an RSA private key for the certificate. 249 | let pkey = gen_rsa_private_key(4096).into_diagnostic()?; 250 | let private_key = pkey.private_key_to_pem_pkcs8().into_diagnostic()?; 251 | let private_key = String::from_utf8(private_key).into_diagnostic()?; 252 | 253 | // Create a certificate signing request for the order, and request 254 | // the certificate. 255 | let order = order 256 | .finalize(Csr::Automatic(pkey)) 257 | .await 258 | .into_diagnostic()?; 259 | 260 | let order = order 261 | .wait_done(Duration::from_secs(5), 10) 262 | .await 263 | .into_diagnostic()?; 264 | ensure!(OrderStatus::Valid == order.status, "Order not Valid"); 265 | 266 | // Download the certificate, and panic if it doesn't exist. 267 | let certificates = order.certificate().await.into_diagnostic()?.unwrap(); 268 | ensure!(certificates.len() > 1, "No certificate returned"); 269 | 270 | let mut bundle = String::new(); 271 | for cert in certificates.clone() { 272 | let cert = cert.to_pem().into_diagnostic()?; 273 | let cert = String::from_utf8(cert).into_diagnostic()?; 274 | bundle += &cert; 275 | } 276 | bundle += &private_key; 277 | Ok(bundle) 278 | } 279 | async fn http_challenge(dns: &str, auth: Authorization, challenge: &Challenge) -> Result<()> { 280 | // Create route to challenge key file 281 | set_challenge_key_file(dns, &challenge).await?; 282 | let unit = make_jucenit_http_challenge_config(dns, &challenge).await?; 283 | unit.push().await?; 284 | 285 | let challenge = challenge.validate().await.into_diagnostic()?; 286 | let challenge = challenge 287 | .wait_done(Duration::from_secs(5), 10) 288 | .await 289 | .into_diagnostic()?; 290 | ensure!( 291 | ChallengeStatus::Valid == challenge.status, 292 | "Http Challenge not Valid" 293 | ); 294 | 295 | // Delete route to challenge key file 296 | del_challenge_key_file(dns, &challenge).await?; 297 | unit.remove().await?; 298 | 299 | let authorization = auth 300 | .wait_done(Duration::from_secs(5), 10) 301 | .await 302 | .into_diagnostic()?; 303 | ensure!( 304 | AuthorizationStatus::Valid == authorization.status, 305 | "Authorization not Valid" 306 | ); 307 | Ok(()) 308 | } 309 | /** 310 | * Warning: Online ressources are relatively poor on how to implement this challenge. 311 | * Refer direcly to the standard at: 312 | * https://datatracker.ietf.org/doc/html/rfc8737 313 | * and read the comments 314 | */ 315 | async fn tls_alpn_challenge( 316 | dns: &str, 317 | auth: Authorization, 318 | challenge: &Challenge, 319 | ) -> Result<()> { 320 | // Create tls cert with challenge info 321 | set_challenge_key_file(dns, &challenge).await?; 322 | 323 | let bundle = make_jucenit_tls_alpn_challenge_config(dns, &challenge)?; 324 | // JuceConfig::add_unit((match_.clone(), unit)).await?; 325 | 326 | let challenge = challenge.validate().await.into_diagnostic()?; 327 | let challenge = challenge 328 | .wait_done(Duration::from_secs(5), 10) 329 | .await 330 | .into_diagnostic()?; 331 | ensure!( 332 | challenge.status == ChallengeStatus::Valid, 333 | "Tls Challenge not Valid" 334 | ); 335 | 336 | // Delete route to challenge key file 337 | del_challenge_key_file(dns, &challenge).await?; 338 | // JuceConfig::del_unit(match_.clone()).await?; 339 | 340 | let authorization = auth 341 | .wait_done(Duration::from_secs(5), 10) 342 | .await 343 | .into_diagnostic()?; 344 | ensure!( 345 | authorization.status == AuthorizationStatus::Valid, 346 | "Authorization not Valid" 347 | ); 348 | Ok(()) 349 | } 350 | } 351 | 352 | #[cfg(test)] 353 | mod tests { 354 | use super::Letsencrypt; 355 | use super::*; 356 | use crate::database::{connect_db, fresh_db}; 357 | use crate::nginx::CertificateStore; 358 | use crate::ConfigFile; 359 | use std::path::PathBuf; 360 | 361 | use miette::Result; 362 | 363 | /** 364 | * Set a fresh testing environment: 365 | * - clean certificate store 366 | * - set minimal nginx configuration 367 | */ 368 | async fn set_testing_config() -> Result<()> { 369 | CertificateStore::clean().await?; 370 | 371 | let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 372 | path.push("../examples/jucenit.toml"); 373 | 374 | let config = ConfigFile::load(path.to_str().unwrap())?; 375 | config.set().await?; 376 | 377 | Ok(()) 378 | } 379 | 380 | #[tokio::test] 381 | async fn set_creds() -> Result<()> { 382 | let res = set_account().await?; 383 | Ok(()) 384 | } 385 | #[tokio::test] 386 | async fn get_cert() -> Result<()> { 387 | set_testing_config().await?; 388 | 389 | let dns = "example.com"; 390 | let account = set_account().await?.clone(); 391 | let res = Letsencrypt::get_cert_bundle(dns, &account).await?; 392 | // println!("{:#?}", res); 393 | Ok(()) 394 | } 395 | } 396 | -------------------------------------------------------------------------------- /jucenit_core/src/ssl/mod.rs: -------------------------------------------------------------------------------- 1 | mod fake; 2 | mod letsencrypt; 3 | pub mod pebble; 4 | // Reexport 5 | pub use fake::Fake; 6 | pub use letsencrypt::{set_account, Letsencrypt}; 7 | -------------------------------------------------------------------------------- /jucenit_core/src/ssl/pebble.rs: -------------------------------------------------------------------------------- 1 | // Global vars 2 | use crate::nginx::SETTINGS; 3 | use once_cell::sync::Lazy; 4 | use std::sync::Arc; 5 | use tokio::sync::Mutex; 6 | // Acme 7 | use acme2::gen_rsa_private_key; 8 | use acme2::{ 9 | Account, AccountBuilder, AuthorizationStatus, Challenge, ChallengeStatus, Csr, Directory, 10 | DirectoryBuilder, OrderBuilder, OrderStatus, 11 | }; 12 | // Error Handling 13 | use miette::{Error, IntoDiagnostic, Result}; 14 | // use acme2::Error; 15 | 16 | // Testing local ACME server 17 | pub static PEBBLE_URL: Lazy>> = 18 | Lazy::new(|| Arc::new(Mutex::new("https://localhost:14000/dir".to_owned()))); 19 | pub static PEBBLE_CERT_URL: Lazy>> = 20 | Lazy::new(|| Arc::new(Mutex::new("https://localhost:15000/roots/0".to_owned()))); 21 | 22 | pub async fn pebble_http_client() -> reqwest::Client { 23 | let raw = tokio::fs::read("/etc/pebble/test/certs/pebble.minica.pem") 24 | .await 25 | .unwrap(); 26 | let cert = reqwest::Certificate::from_pem(&raw).unwrap(); 27 | reqwest::Client::builder() 28 | .add_root_certificate(cert) 29 | .build() 30 | .unwrap() 31 | } 32 | pub async fn pebble_directory() -> Result> { 33 | let http_client = pebble_http_client().await; 34 | let dir = DirectoryBuilder::new(PEBBLE_URL.lock().await.clone()) 35 | .http_client(http_client) 36 | .build() 37 | .await 38 | .into_diagnostic()?; 39 | Ok(dir) 40 | } 41 | -------------------------------------------------------------------------------- /migration/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "migration" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | name = "migration" 9 | path = "src/lib.rs" 10 | 11 | [dependencies] 12 | miette = { version = "7.2.0", features = ["fancy"] } 13 | tokio = { version = "1.37.0", features = ["full"] } 14 | async-std = { version = "1", features = ["attributes", "tokio1"] } 15 | sea-orm = { version = "0.12.15", features = [ 16 | "runtime-tokio-rustls", 17 | "sqlx-sqlite", 18 | "with-json", 19 | "macros", 20 | ] } 21 | strum = { version = "0.26.2", features = ["derive"] } 22 | 23 | [dependencies.sea-orm-migration] 24 | version = "0.12.15" 25 | features = ["runtime-tokio-rustls", "sqlx-sqlite"] 26 | -------------------------------------------------------------------------------- /migration/README.md: -------------------------------------------------------------------------------- 1 | # Running Migrator CLI 2 | 3 | Create the required database schema (tables) 4 | 5 | ```sh 6 | sea-orm-cli migrate fresh 7 | ``` 8 | 9 | Do not use. Entities already exists. 10 | 11 | ```sh 12 | sea-orm-cli generate entity --output-dir ./entity/src 13 | ``` 14 | -------------------------------------------------------------------------------- /migration/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use sea_orm_migration::prelude::*; 2 | 3 | mod m20240606_110915_create_table; 4 | 5 | pub use m20240606_110915_create_table::*; 6 | 7 | pub struct Migrator; 8 | 9 | #[async_trait::async_trait] 10 | impl MigratorTrait for Migrator { 11 | fn migrations() -> Vec> { 12 | vec![Box::new(m20240606_110915_create_table::Migration)] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /migration/src/m20240606_110915_create_table.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! Generate entities. 3 | //! 4 | //! ```sh 5 | //! # on the repo root 6 | //! sea-orm-cli generate entity --output-dir ./entity/src 7 | //! ``` 8 | //! 9 | 10 | use miette::{IntoDiagnostic, Result}; 11 | use sea_orm_migration::prelude::*; 12 | use sea_query::Index; 13 | 14 | #[derive(DeriveMigrationName)] 15 | pub struct Migration; 16 | 17 | #[async_trait::async_trait] 18 | impl MigrationTrait for Migration { 19 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 20 | // Junction table Match_Listener 21 | manager 22 | .create_table( 23 | Table::create() 24 | .table(MatchListener::Table) 25 | .if_not_exists() 26 | .primary_key( 27 | Index::create() 28 | .col(MatchListener::MatchId) 29 | .col(MatchListener::ListenerId), 30 | ) 31 | .col(ColumnDef::new(MatchListener::MatchId).integer().not_null()) 32 | .col( 33 | ColumnDef::new(MatchListener::ListenerId) 34 | .integer() 35 | .not_null(), 36 | ) 37 | .foreign_key( 38 | ForeignKey::create() 39 | .name("fk-match_id") 40 | .from(MatchListener::Table, MatchListener::MatchId) 41 | .to(NgMatch::Table, NgMatch::Id) 42 | .on_delete(ForeignKeyAction::Cascade) 43 | .on_update(ForeignKeyAction::Cascade), 44 | ) 45 | .foreign_key( 46 | ForeignKey::create() 47 | .name("fk-listener_id") 48 | .from(MatchListener::Table, MatchListener::ListenerId) 49 | .to(Listener::Table, Listener::Id) 50 | .on_delete(ForeignKeyAction::Cascade) 51 | .on_update(ForeignKeyAction::Cascade), 52 | ) 53 | .to_owned(), 54 | ) 55 | .await?; 56 | // Junction table Match_Host 57 | manager 58 | .create_table( 59 | Table::create() 60 | .table(MatchHost::Table) 61 | .if_not_exists() 62 | .primary_key( 63 | Index::create() 64 | .col(MatchHost::MatchId) 65 | .col(MatchHost::HostId), 66 | ) 67 | .col(ColumnDef::new(MatchHost::MatchId).integer().not_null()) 68 | .col(ColumnDef::new(MatchHost::HostId).integer().not_null()) 69 | .foreign_key( 70 | ForeignKey::create() 71 | .name("fk-match_id") 72 | .from(MatchHost::Table, MatchHost::MatchId) 73 | .to(NgMatch::Table, NgMatch::Id) 74 | .on_delete(ForeignKeyAction::Cascade) 75 | .on_update(ForeignKeyAction::Cascade), 76 | ) 77 | .foreign_key( 78 | ForeignKey::create() 79 | .name("fk-host_id") 80 | .from(MatchHost::Table, MatchHost::HostId) 81 | .to(Host::Table, Host::Id) 82 | .on_delete(ForeignKeyAction::Cascade) 83 | .on_update(ForeignKeyAction::Cascade), 84 | ) 85 | .to_owned(), 86 | ) 87 | .await?; 88 | // Match 89 | manager 90 | .create_table( 91 | Table::create() 92 | .table(NgMatch::Table) 93 | .if_not_exists() 94 | .col( 95 | ColumnDef::new(NgMatch::Id) 96 | .integer() 97 | .not_null() 98 | .auto_increment() 99 | .primary_key(), 100 | ) 101 | .col(ColumnDef::new(NgMatch::Uuid).uuid().unique_key().not_null()) 102 | .col(ColumnDef::new(NgMatch::ActionId).integer().not_null()) 103 | .foreign_key( 104 | ForeignKey::create() 105 | .name("fk-action_id") 106 | .from(NgMatch::Table, NgMatch::ActionId) 107 | .to(Action::Table, Action::Id), 108 | ) 109 | .col(ColumnDef::new(NgMatch::RawParams).json()) 110 | .to_owned(), 111 | ) 112 | .await?; 113 | // Listener 114 | manager 115 | .create_table( 116 | Table::create() 117 | .table(Listener::Table) 118 | .if_not_exists() 119 | .col( 120 | ColumnDef::new(Listener::Id) 121 | .integer() 122 | .not_null() 123 | .auto_increment() 124 | .primary_key(), 125 | ) 126 | .col( 127 | ColumnDef::new(Listener::IpSocket) 128 | .string() 129 | .not_null() 130 | .unique_key(), 131 | ) 132 | .col(ColumnDef::new(Listener::Tls).json()) 133 | .to_owned(), 134 | ) 135 | .await?; 136 | // Host 137 | manager 138 | .create_table( 139 | Table::create() 140 | .table(Host::Table) 141 | .if_not_exists() 142 | .col( 143 | ColumnDef::new(Host::Id) 144 | .integer() 145 | .not_null() 146 | .auto_increment() 147 | .primary_key(), 148 | ) 149 | .col( 150 | ColumnDef::new(Host::Domain) 151 | .string() 152 | .not_null() 153 | .unique_key(), 154 | ) 155 | .to_owned(), 156 | ) 157 | .await?; 158 | // Action 159 | manager 160 | .create_table( 161 | Table::create() 162 | .table(Action::Table) 163 | .if_not_exists() 164 | .col( 165 | ColumnDef::new(Action::Id) 166 | .integer() 167 | .not_null() 168 | .auto_increment() 169 | .primary_key(), 170 | ) 171 | .col( 172 | ColumnDef::new(Action::RawParams) 173 | .json() 174 | .unique_key() 175 | .not_null(), 176 | ) 177 | .to_owned(), 178 | ) 179 | .await?; 180 | Ok(()) 181 | } 182 | 183 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 184 | // todo!(); 185 | manager 186 | .drop_table(Table::drop().table(Host::Table).to_owned()) 187 | .await?; 188 | manager 189 | .drop_table(Table::drop().table(NgMatch::Table).to_owned()) 190 | .await?; 191 | manager 192 | .drop_table(Table::drop().table(Listener::Table).to_owned()) 193 | .await?; 194 | manager 195 | .drop_table(Table::drop().table(Action::Table).to_owned()) 196 | .await?; 197 | // Pivot/Juction tables 198 | manager 199 | .drop_table(Table::drop().table(MatchListener::Table).to_owned()) 200 | .await?; 201 | manager 202 | .drop_table(Table::drop().table(MatchHost::Table).to_owned()) 203 | .await?; 204 | Ok(()) 205 | } 206 | } 207 | 208 | #[derive(DeriveIden, Debug)] 209 | pub enum MatchListener { 210 | Table, 211 | Id, 212 | MatchId, 213 | ListenerId, 214 | } 215 | #[derive(DeriveIden, Debug)] 216 | pub enum MatchHost { 217 | Table, 218 | Id, 219 | MatchId, 220 | HostId, 221 | } 222 | #[derive(DeriveIden, Debug)] 223 | pub enum Host { 224 | Table, // special attribute 225 | Id, 226 | Domain, // Host domain name (ex: "example.com") 227 | } 228 | 229 | #[derive(DeriveIden, Debug)] 230 | pub enum Listener { 231 | Table, // special attribute 232 | Id, 233 | IpSocket, 234 | Tls, 235 | } 236 | 237 | #[derive(DeriveIden, Debug)] 238 | pub enum NgMatch { 239 | Table, // special attribute 240 | Id, 241 | Uuid, 242 | RawParams, 243 | // Relations 244 | ActionId, 245 | } 246 | 247 | #[derive(DeriveIden)] 248 | pub enum Action { 249 | Table, // special attribute 250 | Id, 251 | RawParams, 252 | } 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use crate::{Migrator, MigratorTrait}; 257 | use miette::{IntoDiagnostic, Result}; 258 | 259 | #[tokio::test] 260 | async fn create_db() -> Result<()> { 261 | let database_url = "sqlite:////var/spool/jucenit/config.sqlite?mode=rwc"; 262 | let connection = sea_orm::Database::connect(database_url) 263 | .await 264 | .into_diagnostic()?; 265 | Migrator::fresh(&connection).await.into_diagnostic()?; 266 | Ok(()) 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /migration/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | cli::run_cli(migration::Migrator).await; 6 | } 7 | 8 | #[cfg(test)] 9 | mod tests { 10 | use super::*; 11 | use miette::Result; 12 | 13 | #[tokio::test] 14 | async fn migrate() -> Result<()> { 15 | cli::run_cli(migration::Migrator).await; 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /module.nix: -------------------------------------------------------------------------------- 1 | { 2 | config, 3 | pkgs, 4 | lib, 5 | inputs, 6 | ... 7 | }: 8 | with lib; let 9 | cfg = config.services.jucenit; 10 | params = { 11 | # Package configuration variables 12 | user = "unit"; 13 | group = "unit"; 14 | stateDir = "/var/spool/unit"; 15 | logDir = "/var/log/unit"; 16 | challengDir = "/tmp/jucenit"; 17 | }; 18 | in { 19 | options.services = { 20 | jucenit.enable = mkEnableOption '' 21 | Toggle the module 22 | ''; 23 | }; 24 | 25 | config = mkIf cfg.enable { 26 | users.users.${params.user} = { 27 | group = "${params.group}"; 28 | isSystemUser = true; 29 | }; 30 | users.groups = { 31 | unit.members = [ 32 | # Add users that can manage unit/jucenit files 33 | "anon" 34 | ]; 35 | }; 36 | 37 | environment.defaultPackages = with pkgs; let 38 | # Get package from flake inputs 39 | jucenit = inputs.jucenit.packages.${system}.default; 40 | in [ 41 | # Web server and dependencies 42 | jucenit 43 | unit 44 | ]; 45 | 46 | systemd.tmpfiles.rules = [ 47 | # Nginx-unit file permissions (bit mode) 48 | "d '${params.stateDir}' 0750 ${params.user} ${params.group} - -" 49 | "Z '${params.stateDir}' 0750 ${params.user} ${params.group} - -" 50 | "d '${params.logDir}' 0750 ${params.user} ${params.group} - -" 51 | "Z '${params.logDir}' 0750 ${params.user} ${params.group} - -" 52 | 53 | # Jucenit file permissions 54 | "d '/var/spool/jucenit' 0774 ${params.user} users - -" 55 | "Z '/var/spool/jucenit' 0774 ${params.user} users - -" 56 | "d '/tmp/jucenit' 774 ${params.user} users - -" 57 | "Z '/tmp/jucenit' 774 ${params.user} users - -" 58 | ]; 59 | 60 | ################################################ 61 | ### Jucenit - autossl 62 | ## Systemd unit 63 | 64 | systemd.services.jucenit_autossl = { 65 | enable = true; 66 | after = ["network.target"]; 67 | wantedBy = ["multi-user.target"]; 68 | serviceConfig = { 69 | ExecStart = with pkgs; let 70 | jucenit = inputs.jucenit.packages.${system}.default; 71 | in '' 72 | ${jucenit} ssl --watch 73 | ''; 74 | ReadWritePaths = [params.stateDir params.logDir params.challengDir]; 75 | }; 76 | }; 77 | 78 | ################################################ 79 | ### Nginx-unit 80 | ## Custom systemd unit 81 | # Replace default secure unix socket with local tcp socket 82 | # source at: https://github.com/NixOS/nixpkgs/nixos/modules/services/web-servers/unit/default.nix 83 | 84 | ## Add global packages 85 | # services.unit.enable = true; # Do not use and prefer custom unit 86 | 87 | systemd.services.unit = let 88 | settings = '' 89 | { 90 | "http": { 91 | "log_route": true, 92 | }, 93 | } 94 | ''; 95 | in { 96 | enable = true; 97 | after = ["network.target"]; 98 | wantedBy = ["multi-user.target"]; 99 | postStart = '' 100 | ${pkgs.curl}/bin/curl -X PUT --data-binary '${settings}' 'http://localhost:8080/config/settings' 101 | ''; 102 | serviceConfig = { 103 | Type = "forking"; 104 | PIDFile = "/run/unit/unit.pid"; 105 | ExecStart = '' 106 | ${pkgs.unit}/bin/unitd \ 107 | --control '127.0.0.1:8080' \ 108 | --pid '/run/unit/unit.pid' \ 109 | --log '${params.logDir}/unit.log' \ 110 | --statedir '${params.stateDir}' \ 111 | --tmpdir '/tmp' \ 112 | --user unit \ 113 | --group unit 114 | ''; 115 | # Runtime directory and mode 116 | RuntimeDirectory = "unit"; 117 | RuntimeDirectoryMode = "0750"; 118 | # Access write directories 119 | ReadWritePaths = [params.stateDir params.logDir]; 120 | # Security 121 | NoNewPrivileges = true; 122 | # Sandboxing 123 | ProtectSystem = "strict"; 124 | ProtectHome = true; 125 | PrivateTmp = true; 126 | PrivateDevices = true; 127 | PrivateUsers = false; 128 | ProtectHostname = true; 129 | ProtectClock = true; 130 | ProtectKernelTunables = true; 131 | ProtectKernelModules = true; 132 | ProtectKernelLogs = true; 133 | ProtectControlGroups = true; 134 | RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"]; 135 | LockPersonality = true; 136 | MemoryDenyWriteExecute = true; 137 | RestrictRealtime = true; 138 | RestrictSUIDSGID = true; 139 | PrivateMounts = true; 140 | # System Call Filtering 141 | SystemCallArchitectures = "native"; 142 | }; 143 | }; 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import {}, 3 | lib, 4 | ... 5 | }: 6 | pkgs.rustPlatform.buildRustPackage rec { 7 | pname = "jucenit"; 8 | version = "0.4.2"; 9 | src = ./.; 10 | 11 | cargoLock = { 12 | lockFile = ./Cargo.lock; 13 | outputHashes = { 14 | # "acme2-0.5.2" = lib.fakeSha256; 15 | "cast-0.5.8" = "sha256-Dva7FTBYaJQiVz+fBLrqFZq0VnNAEhyfIwy5JP2DVPo="; 16 | "acme2-0.5.2" = "sha256-tais3v7bbJDcIJY+WjTxzulKDoDlsXG9VT7MVU/VpLI="; 17 | }; 18 | }; 19 | 20 | # cargoBuildHook = '' 21 | # buildPhase = '' 22 | # cargo build --release 23 | # ''; 24 | # installPhase = '' 25 | # mkdir -p $out/bin 26 | # install -t target/release/${pname} $out/bin 27 | # ''; 28 | # disable tests 29 | checkType = "debug"; 30 | doCheck = false; 31 | 32 | nativeBuildInputs = with pkgs; [ 33 | installShellFiles 34 | openssl.dev 35 | pkg-config 36 | rustc 37 | cargo 38 | ]; 39 | PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; 40 | 41 | # postInstall = with lib; '' 42 | # installShellCompletion --cmd ${pname}\ 43 | # --bash ./autocompletion/${pname}.bash \ 44 | # --fish ./autocompletion/${pname}.fish \ 45 | # --zsh ./autocompletion/_${pname} 46 | # ''; 47 | } 48 | -------------------------------------------------------------------------------- /pipelight.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "https://deno.land/x/pipelight/mod.ts"; 2 | import { pipeline, step } from "https://deno.land/x/pipelight/mod.ts"; 3 | 4 | const name = "dummy"; 5 | 6 | /* 7 | * Upload a dummy self signed certificate to nginx unit 8 | */ 9 | const openssl = pipeline("openssl", () => [ 10 | step("ensure tmp dir", () => ["mkdir -p /tmp/jucenit"]), 11 | step("generate minimal cert", () => [ 12 | `openssl req \ 13 | -x509 -newkey rsa:4096 \ 14 | -sha256 \ 15 | -keyout /tmp/jucenit/key_${name}_minimal.pem \ 16 | -out /tmp/jucenit/cert_${name}_minimal.pem \ 17 | -days 3650 \ 18 | -nodes \ 19 | -subj '/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=example.com'`, 20 | ]), 21 | step("generate cert with info", () => [ 22 | `openssl req \ 23 | -x509 -newkey rsa:4096 \ 24 | -sha256 \ 25 | -keyout /tmp/jucenit/key_${name}.pem \ 26 | -out /tmp/jucenit/cert_${name}.pem \ 27 | -days 3650 \ 28 | -nodes \ 29 | -subj '/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=example.com'`, 30 | ]), 31 | step("generate bundle", () => [ 32 | `cat /tmp/jucenit/cert_${name}.pem /tmp/jucenit/key_${name}.pem > /tmp/jucenit/bundle_dummy.pem`, 33 | ]), 34 | step("update unit", () => [ 35 | `curl -X PUT --data-binary @/tmp/jucenit/bundle_${name}.pem http://localhost:8080/certificates/bundle`, 36 | ]), 37 | ]); 38 | 39 | // Generate certs 40 | const config = { 41 | pipelines: [openssl], 42 | }; 43 | 44 | export default config; 45 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rust-src", "rust-analyzer"] 4 | profile = "default" 5 | targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin"] 6 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {pkgs ? import {}}: 2 | pkgs.mkShell rec { 3 | buildInputs = with pkgs; [ 4 | clang 5 | openssl.dev 6 | pkg-config 7 | # Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16) 8 | llvmPackages.bintools 9 | zlib 10 | rustup 11 | # Need to be installed from inside the shell 12 | # rustup component add 13 | # rust-analyzer # LSP Server 14 | # rustfmt # Formatter 15 | # clippy # Linter 16 | ]; 17 | 18 | # SeaOrm Sqlite database 19 | DATABASE_URL = "sqlite:////var/spool/jucenit/config.sqlite?mode=rwc"; 20 | DBEE_CONNECTIONS = "[ 21 | { 22 | \"name\": \"jucenit_db\", 23 | \"type\": \"sqlite\", 24 | \"url\": \"/var/spool/jucenit/config.sqlite?mode=rwc\" 25 | } 26 | ]"; 27 | 28 | RUSTC_VERSION = pkgs.lib.readFile ./rust-toolchain.toml; 29 | # https://github.com/rust-lang/rust-bindgen#environment-variables 30 | LIBCLANG_PATH = pkgs.lib.makeLibraryPath [pkgs.llvmPackages_latest.libclang.lib]; 31 | shellHook = '' 32 | export PATH=$PATH:''${CARGO_HOME:-~/.cargo}/bin 33 | export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-unknown-linux-gnu/bin/ 34 | export PATH=$PATH:''${RUSTUP_HOME:-~/.rustup}/toolchains/$RUSTC_VERSION-x86_64-apple-darwin/bin/ 35 | ''; 36 | # Add precompiled library to rustc search path 37 | RUSTFLAGS = builtins.map (a: ''-L ${a}/lib'') [ 38 | # add libraries here (e.g. pkgs.libvmi) 39 | ]; 40 | # Add glibc, clang, glib and other headers to bindgen search path 41 | BINDGEN_EXTRA_CLANG_ARGS = 42 | # Includes with normal include path 43 | (builtins.map (a: ''-I"${a}/include"'') [ 44 | # add dev libraries here (e.g. pkgs.libvmi.dev) 45 | pkgs.glibc.dev 46 | ]) 47 | # Includes with special directory paths 48 | ++ [ 49 | ''-I"${pkgs.llvmPackages_latest.libclang.lib}/lib/clang/${pkgs.llvmPackages_latest.libclang.version}/include"'' 50 | ''-I"${pkgs.glib.dev}/include/glib-2.0"'' 51 | ''-I${pkgs.glib.out}/lib/glib-2.0/include/'' 52 | ]; 53 | } 54 | --------------------------------------------------------------------------------