# # # rename "" # to "www/viewmtn" # # add_dir "" # # add_dir "graphs" # # add_dir "projects" # # add_dir "sessions" # # add_dir "skel" # # add_dir "skel/keys" # # add_dir "usher" # # add_dir "www" # # add_dir "www/projects" # # add_file "COPYING" # content [3e9d284b3c48a939e82407d5c3fa6c9e0bf5efcb] # # add_file "Makefile" # content [ce4461777b136adc2471ff2838f756c441aeef20] # # add_file "README" # content [40866c93a1145e0577f5833c7d3902eec4cec515] # # add_file "TODO" # content [2f89d4b97b7d926eda475ff12ba09e7bf2870893] # # add_file "dbrotate.sh" # content [6ef7c7dafab781a45934f4971adfa32856045999] # # add_file "hostconfig.dist" # content [c02e0388a4d297581032deed8cfddae12e7efdbd] # # add_file "install" # content [e3b156b50f3207f6073c7b69c181fdde171c5540] # # add_file "install-config.dist" # content [041ff1bfa044470e29bbc2be1986d196c13384e2] # # add_file "reset" # content [5086bf043c1fc5fbd7e55828a1a993ed31626f66] # # add_file "schema.sql" # content [3bc6dddf39d90aee66ef92a6fc2b25756a8e18e0] # # add_file "skel/keys/address@hidden" # content [9bc20d5e58966d35f2981f121786116c85d4e9ed] # # add_file "skel/monotonerc" # content [e06142b15f04777fd4ef76fc52ab6586f6a87807] # # add_file "usher/AUTHORS" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "usher/COPYING" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "usher/ChangeLog" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "usher/INSTALL" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "usher/Makefile.am" # content [8279856a25e147ea67621157109f9f7e03900e73] # # add_file "usher/NEWS" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "usher/README" # content [dfd06ce97cbe36c3af669ff2ec827a5e42d6bb76] # # add_file "usher/administrator.cc" # content [774eb6e37ccf5a2a2e6f4721198e02874dc353e9] # # add_file "usher/administrator.hh" # content [2e5057dacae5de057174d7424c7b572ab1c72258] # # add_file "usher/basic_io_serverlist_reader.cc" # content [1222bec508bf6134ef5d15165af8e0ebbde1bde3] # # add_file "usher/basic_io_serverlist_reader.hh" # content [19f9f608cf418f7662b957a68649771294711bac] # # add_file "usher/bootstrap" # content [59bc8c05eea55a0dfafe641e8c976315b3da23bc] # # add_file "usher/buffer.cc" # content [430ce3374bb6938b832e57e9636a8cfcbe67d0b9] # # add_file "usher/buffer.hh" # content [4c747ee60d3e481cd03b17a4dff0d75a1dc7f474] # # add_file "usher/channel.cc" # content [52d8c79fb85d032b9e89cd6e5aa2413fad04f084] # # add_file "usher/channel.hh" # content [e7c35bd545765f9aa0908a1b955132bb3ba69802] # # add_file "usher/configure.in" # content [a191b18290b0903b951d62b95ddd9a4f8e769e0b] # # add_file "usher/dir_serverlist_reader.cc" # content [eb07a697b97f3580bb5c5ffbf11a27ba250b6cfa] # # add_file "usher/dir_serverlist_reader.hh" # content [8ce0af271ee1f8e8e474b5ed7689a420c342342e] # # add_file "usher/err.cc" # content [2fef2a9feb7078a0f30ed64c6a1789ddfca7f5bb] # # add_file "usher/err.hh" # content [7880a96e6e31043b0079d2641107f20afbbcb016] # # add_file "usher/forked_server_manager.cc" # content [8ae502d0befd209b2b7e31c1e08af8d2cfeda90a] # # add_file "usher/forked_server_manager.hh" # content [220b035b43a7f62fa99938d23ab7dc8b2f4ba3fd] # # add_file "usher/io.cc" # content [b0753ee961d8799ad24f008d6497132fc07ad9c2] # # add_file "usher/io.hh" # content [ccad8be31096e6ca11cae88425b5986ea258f07c] # # add_file "usher/query_client.cc" # content [4aaedbdb9403d7cb33f459a7a0b6e500716cc341] # # add_file "usher/query_client.hh" # content [e45fb70cd2feee54885cded16c277071ff0b8892] # # add_file "usher/server.cc" # content [dc8713e8c772cd307cecafb7d3c47a63bd92a4e2] # # add_file "usher/server.hh" # content [3b06fa45604b1a9aea829006c630a5595e7dff96] # # add_file "usher/server_manager.cc" # content [36910e7007c355bd549ebd61555afde8e440b59c] # # add_file "usher/server_manager.hh" # content [5dfcfa92583c640562517653691199f91250969c] # # add_file "usher/serverlist_reader.cc" # content [5cf9b3232eb61007105bc92eb73f9a04368ae035] # # add_file "usher/serverlist_reader.hh" # content [9613a97c615bfb147dd4910eedc8cb1d9b9150f7] # # add_file "usher/sock.cc" # content [c7d1253b571db327663956556a53969b06defbbb] # # add_file "usher/sock.hh" # content [4b136726ab92c01b4378e5bb1c1b56e2a4c11acf] # # add_file "usher/usher.cc" # content [24cdda41ec47e215ab8ea0223ff9e9b26d3cfe41] # # add_file "usher.cc" # content [8ab88aaf43ec1469fdd3cc95b9cfabb1d1b517e0] # # add_file "webhost.conf" # content [5bcbdd89e688a5104075fd4323560238070012ef] # # add_file "webhost.cronjob" # content [839760463ef4f93e3f346df0559805db3506842a] # # add_file "webhost.initscript" # content [bf5f34cb46dc189669206a1c54aa4b14c8611738] # # add_file "www/JSON.php" # content [e74c9792a17996ea4e2ab7c313e24238e8022650] # # add_file "www/about.php" # content [c77f4adb39b1e74bd1e936be60e008e069d020ee] # # add_file "www/admin-description.js" # content [d8fab719409fc04467c21a9670d51ebd8b1ebbc1] # # add_file "www/admin-description.php" # content [f4d40babaff2b48eb133853bd2eb938bf01ce084] # # add_file "www/admin-description_backend.php" # content [31ebbd661f9f479a0238ebec278fbb2636bd3774] # # add_file "www/admin-files.js" # content [1d84e9f90e267d99e4bddffe5f2e3185e31a0952] # # add_file "www/admin-files.php" # content [cf293356f2e2925a5212262f24965413fd10d1d2] # # add_file "www/admin-files_backend.php" # content [8109ee3be651cbd4d693b930060403c1a7149a46] # # add_file "www/admin-permissions.js" # content [444fcd22988b44ffc3df1f64ece814c0e734d889] # # add_file "www/admin-permissions.php" # content [1b1e6d4dc769ca39aa9dd01a5a8f7b3c4438c2bc] # # add_file "www/admin-permissions_backend.php" # content [fe7eeb8b0b6ad5f905dc314e51aee3e505852ed2] # # add_file "www/admin-resources.js" # content [de96b33ae298c6bba164631c769e420b658fbcda] # # add_file "www/admin-resources.php" # content [cc9f71016f146498bc68832f99a8bbb8c88fe426] # # add_file "www/admin-resources_backend.php" # content [c25e06b302ff66e55ecee79a85765f2e90339d2e] # # add_file "www/admin-source_control.js" # content [a082da5d4a5e50cb274bb9328c9f9b1c2118043b] # # add_file "www/admin-source_control.php" # content [700d93d02ecec10bccee13823d193fcddd1b6ed3] # # add_file "www/admin-source_control_backend.php" # content [9379456da9dab9d28315a8be28a5f7a99e123ccf] # # add_file "www/admin-web.js" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "www/admin-web.php" # content [011cc85d5bd32650ca04e0b1b40f2dfd40e1b19e] # # add_file "www/admin-web_backend.php" # content [da39a3ee5e6b4b0d3255bfef95601890afd80709] # # add_file "www/admin.php" # content [fbd0d8196092c77c9c4fbd917f242cf2b113e79a] # # add_file "www/administrator.js" # content [6c90694cc02ec84ca391741656b00c9f4ef6692d] # # add_file "www/common-ctrl.php" # content [5ea62c8285f5c48fad5e55441be84f6a8f484bec] # # add_file "www/common-resource.php" # content [fbb14c31a5b356f9199190f90cee387a3c9a247d] # # add_file "www/common.js" # content [775a226f88575520d8f512e38400ca78a4f85168] # # add_file "www/common.php" # content [b3d58f037ac01cd79b36aa512d985a4f5b53b281] # # add_file "www/index.php" # content [264a470d6d8750348eda8095abbd390b8dc0dc68] # # add_file "www/login.php" # content [d22d0cba163de0db350017eb0427c7bdd7e0abe2] # # add_file "www/logo.png" # content [e6c7794bf02d16122ecfe0906eaa74cac0b3e17e] # # add_file "www/main.css" # content [ab84d1bfabe8c74ba8b62ab6835d0d478419abf1] # # add_file "www/proj-ctrl.php" # content [2c4cb05995c4be6da7991a071143eb2ada8a327c] # # add_file "www/project.php" # content [22f0453de251f599f486f4e43cdc4f660548ccc5] # # add_file "www/sidebar.js" # content [591674ba9a1f37b50fe779982c88caaca5aa0145] # # add_file "www/sidebar.php" # content [bf888eca0fe896772daa4a00fe755d6ecfb1aa1b] # # add_file "www/top.php" # content [08f6997db49e7d8c65c79ea404da9d98b9c900c3] # # add_file "www/user-actions.php" # content [aa3667da94ee7a18543bbb487d3e658704602c20] # # add_file "www/user-ctrl.php" # content [9f01d97b2c58c2a1b8c547782d105f3edf093260] # # add_file "www/user.php" # content [877d668d0f7964e6929ddfab3f07d48483e39475] # # add_file "www/userlist.php" # content [003c0da953c921c559edb0250e8cf9196ecea768] # # patch "www/viewmtn/config.py.example" # from [9248282147264a3a9d09df9de7337b672e2d41f2] # to [97fbc51fb12393018862a3073aa718a25bfb6eac] # # set "dbrotate.sh" # attr "mtn:execute" # value "true" # # set "install" # attr "mtn:execute" # value "true" # # set "reset" # attr "mtn:execute" # value "true" # # set "usher/bootstrap" # attr "mtn:execute" # value "true" # # set "webhost.cronjob" # attr "mtn:execute" # value "true" # # set "webhost.initscript" # attr "mtn:execute" # value "true" # # set "www/logo.png" # attr "mtn:manual_merge" # value "true" # ============================================================ --- COPYING 3e9d284b3c48a939e82407d5c3fa6c9e0bf5efcb +++ COPYING 3e9d284b3c48a939e82407d5c3fa6c9e0bf5efcb @@ -0,0 +1,724 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + + +GNU Free Documentation License +****************************** + + Version 1.1, March 2000 + Copyright (C) 2000 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + 0. PREAMBLE + + The purpose of this License is to make a manual, textbook, or other + written document "free" in the sense of freedom: to assure everyone + the effective freedom to copy and redistribute it, with or without + modifying it, either commercially or noncommercially. Secondarily, + this License preserves for the author and publisher a way to get + credit for their work, while not being considered responsible for + modifications made by others. + + This License is a kind of "copyleft", which means that derivative + works of the document must themselves be free in the same sense. + It complements the GNU General Public License, which is a copyleft + license designed for free software. + + We have designed this License in order to use it for manuals for + free software, because free software needs free documentation: a + free program should come with manuals providing the same freedoms + that the software does. But this License is not limited to + software manuals; it can be used for any textual work, regardless + of subject matter or whether it is published as a printed book. + We recommend this License principally for works whose purpose is + instruction or reference. + + 1. APPLICABILITY AND DEFINITIONS + + This License applies to any manual or other work that contains a + notice placed by the copyright holder saying it can be distributed + under the terms of this License. The "Document", below, refers to + any such manual or work. Any member of the public is a licensee, + and is addressed as "you". + + A "Modified Version" of the Document means any work containing the + Document or a portion of it, either copied verbatim, or with + modifications and/or translated into another language. + + A "Secondary Section" is a named appendix or a front-matter + section of the Document that deals exclusively with the + relationship of the publishers or authors of the Document to the + Document's overall subject (or to related matters) and contains + nothing that could fall directly within that overall subject. + (For example, if the Document is in part a textbook of + mathematics, a Secondary Section may not explain any mathematics.) + The relationship could be a matter of historical connection with + the subject or with related matters, or of legal, commercial, + philosophical, ethical or political position regarding them. + + The "Invariant Sections" are certain Secondary Sections whose + titles are designated, as being those of Invariant Sections, in + the notice that says that the Document is released under this + License. + + The "Cover Texts" are certain short passages of text that are + listed, as Front-Cover Texts or Back-Cover Texts, in the notice + that says that the Document is released under this License. + + A "Transparent" copy of the Document means a machine-readable copy, + represented in a format whose specification is available to the + general public, whose contents can be viewed and edited directly + and straightforwardly with generic text editors or (for images + composed of pixels) generic paint programs or (for drawings) some + widely available drawing editor, and that is suitable for input to + text formatters or for automatic translation to a variety of + formats suitable for input to text formatters. A copy made in an + otherwise Transparent file format whose markup has been designed + to thwart or discourage subsequent modification by readers is not + Transparent. A copy that is not "Transparent" is called "Opaque". + + Examples of suitable formats for Transparent copies include plain + ASCII without markup, Texinfo input format, LaTeX input format, + SGML or XML using a publicly available DTD, and + standard-conforming simple HTML designed for human modification. + Opaque formats include PostScript, PDF, proprietary formats that + can be read and edited only by proprietary word processors, SGML + or XML for which the DTD and/or processing tools are not generally + available, and the machine-generated HTML produced by some word + processors for output purposes only. + + The "Title Page" means, for a printed book, the title page itself, + plus such following pages as are needed to hold, legibly, the + material this License requires to appear in the title page. For + works in formats which do not have any title page as such, "Title + Page" means the text near the most prominent appearance of the + work's title, preceding the beginning of the body of the text. + + 2. VERBATIM COPYING + + You may copy and distribute the Document in any medium, either + commercially or noncommercially, provided that this License, the + copyright notices, and the license notice saying this License + applies to the Document are reproduced in all copies, and that you + add no other conditions whatsoever to those of this License. You + may not use technical measures to obstruct or control the reading + or further copying of the copies you make or distribute. However, + you may accept compensation in exchange for copies. If you + distribute a large enough number of copies you must also follow + the conditions in section 3. + + You may also lend copies, under the same conditions stated above, + and you may publicly display copies. + + 3. COPYING IN QUANTITY + + If you publish printed copies of the Document numbering more than + 100, and the Document's license notice requires Cover Texts, you + must enclose the copies in covers that carry, clearly and legibly, + all these Cover Texts: Front-Cover Texts on the front cover, and + Back-Cover Texts on the back cover. Both covers must also clearly + and legibly identify you as the publisher of these copies. The + front cover must present the full title with all words of the + title equally prominent and visible. You may add other material + on the covers in addition. Copying with changes limited to the + covers, as long as they preserve the title of the Document and + satisfy these conditions, can be treated as verbatim copying in + other respects. + + If the required texts for either cover are too voluminous to fit + legibly, you should put the first ones listed (as many as fit + reasonably) on the actual cover, and continue the rest onto + adjacent pages. + + If you publish or distribute Opaque copies of the Document + numbering more than 100, you must either include a + machine-readable Transparent copy along with each Opaque copy, or + state in or with each Opaque copy a publicly-accessible + computer-network location containing a complete Transparent copy + of the Document, free of added material, which the general + network-using public has access to download anonymously at no + charge using public-standard network protocols. If you use the + latter option, you must take reasonably prudent steps, when you + begin distribution of Opaque copies in quantity, to ensure that + this Transparent copy will remain thus accessible at the stated + location until at least one year after the last time you + distribute an Opaque copy (directly or through your agents or + retailers) of that edition to the public. + + It is requested, but not required, that you contact the authors of + the Document well before redistributing any large number of + copies, to give them a chance to provide you with an updated + version of the Document. + + 4. MODIFICATIONS + + You may copy and distribute a Modified Version of the Document + under the conditions of sections 2 and 3 above, provided that you + release the Modified Version under precisely this License, with + the Modified Version filling the role of the Document, thus + licensing distribution and modification of the Modified Version to + whoever possesses a copy of it. In addition, you must do these + things in the Modified Version: + + A. Use in the Title Page (and on the covers, if any) a title + distinct from that of the Document, and from those of + previous versions (which should, if there were any, be listed + in the History section of the Document). You may use the + same title as a previous version if the original publisher of + that version gives permission. + + B. List on the Title Page, as authors, one or more persons or + entities responsible for authorship of the modifications in + the Modified Version, together with at least five of the + principal authors of the Document (all of its principal + authors, if it has less than five). + + C. State on the Title page the name of the publisher of the + Modified Version, as the publisher. + + D. Preserve all the copyright notices of the Document. + + E. Add an appropriate copyright notice for your modifications + adjacent to the other copyright notices. + + F. Include, immediately after the copyright notices, a license + notice giving the public permission to use the Modified + Version under the terms of this License, in the form shown in + the Addendum below. + + G. Preserve in that license notice the full lists of Invariant + Sections and required Cover Texts given in the Document's + license notice. + + H. Include an unaltered copy of this License. + + I. Preserve the section entitled "History", and its title, and + add to it an item stating at least the title, year, new + authors, and publisher of the Modified Version as given on + the Title Page. If there is no section entitled "History" in + the Document, create one stating the title, year, authors, + and publisher of the Document as given on its Title Page, + then add an item describing the Modified Version as stated in + the previous sentence. + + J. Preserve the network location, if any, given in the Document + for public access to a Transparent copy of the Document, and + likewise the network locations given in the Document for + previous versions it was based on. These may be placed in + the "History" section. You may omit a network location for a + work that was published at least four years before the + Document itself, or if the original publisher of the version + it refers to gives permission. + + K. In any section entitled "Acknowledgments" or "Dedications", + preserve the section's title, and preserve in the section all + the substance and tone of each of the contributor + acknowledgments and/or dedications given therein. + + L. Preserve all the Invariant Sections of the Document, + unaltered in their text and in their titles. Section numbers + or the equivalent are not considered part of the section + titles. + + M. Delete any section entitled "Endorsements". Such a section + may not be included in the Modified Version. + + N. Do not retitle any existing section as "Endorsements" or to + conflict in title with any Invariant Section. + + If the Modified Version includes new front-matter sections or + appendices that qualify as Secondary Sections and contain no + material copied from the Document, you may at your option + designate some or all of these sections as invariant. To do this, + add their titles to the list of Invariant Sections in the Modified + Version's license notice. These titles must be distinct from any + other section titles. + + You may add a section entitled "Endorsements", provided it contains + nothing but endorsements of your Modified Version by various + parties--for example, statements of peer review or that the text + has been approved by an organization as the authoritative + definition of a standard. + + You may add a passage of up to five words as a Front-Cover Text, + and a passage of up to 25 words as a Back-Cover Text, to the end + of the list of Cover Texts in the Modified Version. Only one + passage of Front-Cover Text and one of Back-Cover Text may be + added by (or through arrangements made by) any one entity. If the + Document already includes a cover text for the same cover, + previously added by you or by arrangement made by the same entity + you are acting on behalf of, you may not add another; but you may + replace the old one, on explicit permission from the previous + publisher that added the old one. + + The author(s) and publisher(s) of the Document do not by this + License give permission to use their names for publicity for or to + assert or imply endorsement of any Modified Version. + + 5. COMBINING DOCUMENTS + + You may combine the Document with other documents released under + this License, under the terms defined in section 4 above for + modified versions, provided that you include in the combination + all of the Invariant Sections of all of the original documents, + unmodified, and list them all as Invariant Sections of your + combined work in its license notice. + + The combined work need only contain one copy of this License, and + multiple identical Invariant Sections may be replaced with a single + copy. If there are multiple Invariant Sections with the same name + but different contents, make the title of each such section unique + by adding at the end of it, in parentheses, the name of the + original author or publisher of that section if known, or else a + unique number. Make the same adjustment to the section titles in + the list of Invariant Sections in the license notice of the + combined work. + + In the combination, you must combine any sections entitled + "History" in the various original documents, forming one section + entitled "History"; likewise combine any sections entitled + "Acknowledgments", and any sections entitled "Dedications". You + must delete all sections entitled "Endorsements." + + 6. COLLECTIONS OF DOCUMENTS + + You may make a collection consisting of the Document and other + documents released under this License, and replace the individual + copies of this License in the various documents with a single copy + that is included in the collection, provided that you follow the + rules of this License for verbatim copying of each of the + documents in all other respects. + + You may extract a single document from such a collection, and + distribute it individually under this License, provided you insert + a copy of this License into the extracted document, and follow + this License in all other respects regarding verbatim copying of + that document. + + 7. AGGREGATION WITH INDEPENDENT WORKS + + A compilation of the Document or its derivatives with other + separate and independent documents or works, in or on a volume of + a storage or distribution medium, does not as a whole count as a + Modified Version of the Document, provided no compilation + copyright is claimed for the compilation. Such a compilation is + called an "aggregate", and this License does not apply to the + other self-contained works thus compiled with the Document, on + account of their being thus compiled, if they are not themselves + derivative works of the Document. + + If the Cover Text requirement of section 3 is applicable to these + copies of the Document, then if the Document is less than one + quarter of the entire aggregate, the Document's Cover Texts may be + placed on covers that surround only the Document within the + aggregate. Otherwise they must appear on covers around the whole + aggregate. + + 8. TRANSLATION + + Translation is considered a kind of modification, so you may + distribute translations of the Document under the terms of section + 4. Replacing Invariant Sections with translations requires special + permission from their copyright holders, but you may include + translations of some or all Invariant Sections in addition to the + original versions of these Invariant Sections. You may include a + translation of this License provided that you also include the + original English version of this License. In case of a + disagreement between the translation and the original English + version of this License, the original English version will prevail. + + 9. TERMINATION + + You may not copy, modify, sublicense, or distribute the Document + except as expressly provided for under this License. Any other + attempt to copy, modify, sublicense or distribute the Document is + void, and will automatically terminate your rights under this + License. However, parties who have received copies, or rights, + from you under this License will not have their licenses + terminated so long as such parties remain in full compliance. + + 10. FUTURE REVISIONS OF THIS LICENSE + + The Free Software Foundation may publish new, revised versions of + the GNU Free Documentation License from time to time. Such new + versions will be similar in spirit to the present version, but may + differ in detail to address new problems or concerns. See + `http://www.gnu.org/copyleft/'. + + Each version of the License is given a distinguishing version + number. If the Document specifies that a particular numbered + version of this License "or any later version" applies to it, you + have the option of following the terms and conditions either of + that specified version or of any later version that has been + published (not as a draft) by the Free Software Foundation. If + the Document does not specify a version number of this License, + you may choose any version ever published (not as a draft) by the + Free Software Foundation. + +ADDENDUM: How to use this License for your documents +---------------------------------------------------- + + To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and license +notices just after the title page: + + Copyright (C) YEAR YOUR NAME. + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.1 + or any later version published by the Free Software Foundation; + with the Invariant Sections being LIST THEIR TITLES, with the + Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. + A copy of the license is included in the section entitled ``GNU + Free Documentation License''. + + If you have no Invariant Sections, write "with no Invariant Sections" +instead of saying which ones are invariant. If you have no Front-Cover +Texts, write "no Front-Cover Texts" instead of "Front-Cover Texts being +LIST"; likewise for Back-Cover Texts. + + If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, to +permit their use in free software. + ============================================================ --- Makefile ce4461777b136adc2471ff2838f756c441aeef20 +++ Makefile ce4461777b136adc2471ff2838f756c441aeef20 @@ -0,0 +1,18 @@ +.PHONY: all install forced-install + +all: usher.webhost + +#usher.webhost: usher.cc +# g++ usher.cc -o usher.webhost -g -Wall +usher.webhost: ALWAYS + cd usher && \ + [ -s configure ] || ./bootstrap && \ + [ -s Makefile ] || ./configure && make + cp usher/usher usher.webhost + +ALWAYS: + +install: usher.webhost + ./install +forced-install: usher.webhost + ./install --force-conffiles ============================================================ --- README 40866c93a1145e0577f5833c7d3902eec4cec515 +++ README 40866c93a1145e0577f5833c7d3902eec4cec515 @@ -0,0 +1,17 @@ +Install +------- + +Make sure you have mod_python and mod_php. You also need libphp-adodb and +a database. The install defaults assume postgres. + + +make install + + +About +----- + +This is a server for shared, public monotone hosting. It has a web frontend +for general project management, and provides web-based history browsing with +viewmtn. The project repositories are identified by hostname: if the server is +example.net, the 'foobar' project repository will be at foobar.example.net . ============================================================ --- TODO 2f89d4b97b7d926eda475ff12ba09e7bf2870893 +++ TODO 2f89d4b97b7d926eda475ff12ba09e7bf2870893 @@ -0,0 +1,5 @@ +www/admin-web* + maybe have webspace? + +www/admin-resources* + more resource types ============================================================ --- dbrotate.sh 6ef7c7dafab781a45934f4971adfa32856045999 +++ dbrotate.sh 6ef7c7dafab781a45934f4971adfa32856045999 @@ -0,0 +1,31 @@ +#!/bin/sh + +queue() +{ + ln -s x rotate_is_queued 2>/dev/null + $0 run $1 & +} + +run() +{ + if ln -s x rotate_is_running; then + while [ -L rotate_is_queued ]; do + rm rotate_is_queued + PROJ=$(basename $1) + HOST=$(grep '^hostname' CONFDIR/hostconfig \ + | sed 's/^[^"]*"\([^"]*\)"[^"]*$/\1/g') + HOSTNAME=$PROJ.$HOST + MONOTONE=$(grep '^monotone' CONFDIR/hostconfig \ + | sed 's/^[^"]*"\([^"]*\)"[^"]*$/\1/g') + $MONOTONE pull -d database.transfer --norc $HOSTNAME \* + ln database.viewmtn database.temp + mv database.transfer database.viewmtn + mv database.temp database.transfer + done + rm rotate_is_running + fi +} +cd $2 +if [ "$1" == "queue" ] || [ "$1" == "run" ]; then + $@; : >/dev/null 2>&1 +fi ============================================================ --- hostconfig.dist c02e0388a4d297581032deed8cfddae12e7efdbd +++ hostconfig.dist c02e0388a4d297581032deed8cfddae12e7efdbd @@ -0,0 +1,26 @@ +userpass "username" "password" +adodb_path "/usr/share/adodb/adodb.inc.php" +dbstring "postgres://DBUSER:address@hidden/DBNAME" +adminaddr "127.0.0.5:12345" + +site_owner_email "OWNER" + +logdir "LOGDIR" + +hostkey "address@hidden" +hostkeypass "address@hidden" +monotone "MONOTONE" + +graphdir_size "10000" + +hostname "HOSTNAME" + +base_url "BASEURL" + +project_dir "PROJECTDIR" + +graph_dir "GRAPHDIR" + +www_dir "WWWDIR" + +comment "foo" ============================================================ --- install e3b156b50f3207f6073c7b69c181fdde171c5540 +++ install e3b156b50f3207f6073c7b69c181fdde171c5540 @@ -0,0 +1,178 @@ +#!/bin/sh + +if [ -f ./install-config ]; then + . ./install-config +else + echo "You need to copy install-config.dist to install-config and edit the values," + echo "then run this script again" + exit 1 +fi + +prompt_about_file_update() { + FILE="$1" + + while /bin/true; do + echo "$FILE is updated" + echo "Right now, the update is in $FILE.new" + echo "What do you want to do?" + echo " I or Y - Install the new version" + echo " N - Leave the old version alone" + echo " D - See the difference" + echo " Z - Escape to $SHELL" + read ANSWER + ANSWER=`echo $ANSWER | tr a-z A-Z` + case $ANSWER in + I*|Y*) + /bin/true + return + ;; + N*) + /bin/false + return + ;; + D*) + diff -u $FILE $FILE.new + ;; + Z*) + echo "Get back here by simply exiting the shell" + $SHELL + ;; + esac + done +} +install_file() { + FILE="$1" + CHECK="$2" + + if $CHECK && [ -f $FILE ] && ! cmp $FILE $FILE.new; then + if prompt_about_file_update "$FILE"; then + mv $FILE $FILE.old + mv $FILE.new $FILE + echo "Replaced the old $FILE" + echo "The old file is backed up as $FILE.old" + else + echo "There is an update for $FILE in $FILE.new" + fi + else + mv $FILE.new $FILE + echo "Installed $FILE" + fi +} + +do_install() { + umask 002 + mkdir -p $PROJDIR/ + mkdir -p $GRAPHDIR/ + mkdir -p $LOGDIR/ + + mkdir -p $WWWDIR/ + # sort -r is a trick to hopefully get the longest paths first + find $WWWDIR -type d | grep -v "^$WWWDIR/projects" | sort -r | \ + while read D; do + rm -f $D/* $D/.* 2>/dev/null + rmdir $D 2>/dev/null + echo "Removed $D and its contents" + done + # The main directory migh just have gotten destroyed, let's recreate it + mkdir -p $WWWDIR/ + (cd www; tar -cf - .) | (cd $WWWDIR; tar -xf -) + chmod -R a+r $WWWDIR/ + echo "Copied www/... to $WWWDIR/..." + + rm -f $WWWDIR/MochiKit + ln -s viewmtn/MochiKit/ $WWWDIR/MochiKit + rm -f $WWWDIR/viewmtn/graph + ln -s $GRAPHDIR/ $WWWDIR/viewmtn/graph + # the webserver needs to be able to link to certain files + chown $WEBUSER $WWWDIR/project.php + chown $WEBUSER $WWWDIR/admin.php + chown $WEBUSER $WWWDIR/viewmtn/ + # the webserver needs to be able to write to certain dirs + chown $WEBUSER $GRAPHDIR/ $WWWDIR/viewmtn/graph + chown $WEBUSER $PROJDIR/ $WWWDIR/projects/ + # the usher needs to be able to write logs + chown $USHERUSER $LOGDIR/ + + make usher.webhost + cp usher.webhost $BINDIR/usher.$BINNAME + if [ ! -d $CONFDIR ] || [ "$1" == "--force-conffiles" ]; then + if [ -d $CONFDIR ]; then + echo "Replacing your configuration..." + else + mkdir $CONFDIR/ + fi + if [ ! -f $CONFDIR/secfile ]; then + dd if=/dev/random of=$CONFDIR/secfile bs=1 count=64 + chown $WEBUSER $CONFDIR/secfile + chmod 0400 $CONFDIR/secfile + fi + + sed -e "s!CONFDIR!$CONFDIR!" \ + < dbrotate.sh > $BINDIR/$BINNAME.update-viewmtn-db.sh.new + chmod a+x $BINDIR/$BINNAME.update-viewmtn-db.sh.new + install_file $BINDIR/$BINNAME.update-viewmtn-db.sh /bin/false + + sed -e "s!DBROTATE!$BINDIR/$BINNAME.update-viewmtn-db.sh!" \ + -e "s!CONFDIR!$CONFDIR!" \ + < skel/monotonerc > $CONFDIR/monotonerc.new + install_file $CONFDIR/monotonerc /bin/false + + sed -e "s!HOSTNAME!$HOSTNAME!" \ + -e "s!DBHOST!$DBHOST!" \ + -e "s!DBNAME!$DBNAME!" \ + -e "s!DBUSER!$DBUSER!" \ + -e "s!DBPASS!$DBPASS!" \ + -e "s!MONOTONE!$MONOTONE!" \ + -e "s!BASEURL!http://$HOSTNAME/$NAME/!" \ + -e "s!PROJECTDIR!$PROJDIR!" \ + -e "s!GRAPHDIR!$GRAPHDIR!" \ + -e "s!WWWDIR!$WWWDIR!" \ + -e "s!LOGDIR!$LOGDIR!" \ + -e "s!OWNER!$OWNER!" \ + < hostconfig.dist > $CONFDIR/hostconfig.new + install_file $CONFDIR/hostconfig /bin/true + + sed -e "s!BINDIR!$BINDIR!" \ + -e "s!BINNAME!$BINNAME!" \ + -e "s!CONFDIR!$CONFDIR!" \ + -e "s!USHERUSER!$USHERUSER!" \ + -e "s!NAME!$NAME!" \ + < webhost.initscript > /etc/init.d/$NAME.new + chmod a+x /etc/init.d/$NAME.new + install_file /etc/init.d/$NAME /bin/false + + sed -e "s!GRAPHDIR!$GRAPHDIR!" \ + -e "s!CONFDIR!$CONFDIR!" \ + < webhost.cronjob > /etc/cron.daily/$NAME.new + install_file /etc/cron.daily/$NAME /bin/false + + sed -e "s!NAME!$NAME!" \ + -e "s!WWWDIR!$WWWDIR!" \ + < webhost.conf > /etc/apache2/conf.d/$NAME.new + install_file /etc/apache2/conf.d/$NAME /bin/true + + cp -r skel/keys $CONFDIR/ + fi + + sed -e "s!'CONFDIR'!'$CONFDIR'!" \ + < $WWWDIR/common.php > $WWWDIR/common.php.new + install_file $WWWDIR/common.php /bin/false + + if [ -f $WWWDIR/viewmtn/config.py.example ]; then + sed -e "s!'CONFDIR'!'$CONFDIR'!" \ + < $WWWDIR/viewmtn/config.py.example \ + > $WWWDIR/viewmtn/config.py.new + install_file $WWWDIR/viewmtn/config.py /bin/false + fi + + if psql -c "\d" $DBNAME 2>/dev/null >/dev/null; then + : + else + createuser -A -D -P $DBUSER + createdb $DBNAME + sed -e "s!DBUSER!$DBUSER!" < schema.sql | psql -f- $DBNAME + fi +} + +do_install "$1" + ============================================================ --- install-config.dist 041ff1bfa044470e29bbc2be1986d196c13384e2 +++ install-config.dist 041ff1bfa044470e29bbc2be1986d196c13384e2 @@ -0,0 +1,23 @@ +#!/bin/sh + +WEBUSER=www-data +WEBGROUP=www-data +USHERUSER=www-data +USHERGROUP=www-data +NAME=webhost +BINNAME=webhost +DBHOST=127.0.0.1 +DBNAME=webhost-db +DBUSER=$WEBUSER +DBPASS=dbpassword +CONFDIR=/etc/$NAME +PROJDIR=/var/lib/$NAME/projects +GRAPHDIR=/var/cache/$NAME/graphs +WWWDIR=/var/www/$NAME +LOGDIR=/var/log/$NAME +BINDIR=/usr/bin +MONOTONE=/usr/bin/mtn + +HOSTNAME=$(hostname) + address@hidden ============================================================ --- reset 5086bf043c1fc5fbd7e55828a1a993ed31626f66 +++ reset 5086bf043c1fc5fbd7e55828a1a993ed31626f66 @@ -0,0 +1,7 @@ +#!/bin/sh + +psql -c 'delete from users;' testdb +psql -c 'delete from projects;' testdb +psql -c 'delete from permissions;' testdb +rm -rf projects/* +rm -rf www/projects/* ============================================================ --- schema.sql 3bc6dddf39d90aee66ef92a6fc2b25756a8e18e0 +++ schema.sql 3bc6dddf39d90aee66ef92a6fc2b25756a8e18e0 @@ -0,0 +1,29 @@ + +CREATE TABLE users ( username varchar(80), + password char(40), + admin smallint, + email varchar(40), + active smallint, + primary key (username) ); + +CREATE TABLE projects ( name varchar(80), + primary key (name) ); + +CREATE TABLE permissions ( username varchar(80), + project varchar(80), + give smallint, + upload smallint, + homepage smallint, + access smallint, + server smallint, + description smallint, + primary key (username, project) ); + +CREATE TABLE resources ( project varchar(80), + name varchar(80), + type varchar(80), + data varchar(160), + primary key (project) + ); + +GRANT ALL PRIVILEGES ON users, projects, permissions, resources TO "DBUSER"; ============================================================ --- skel/keys/address@hidden 9bc20d5e58966d35f2981f121786116c85d4e9ed +++ skel/keys/address@hidden 9bc20d5e58966d35f2981f121786116c85d4e9ed @@ -0,0 +1,19 @@ +[keypair address@hidden +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMYG6FSLV6gI1k6WXYOd+SWZllh2EZi0a9 +aB+tqyUneitVpG8YN9JJfLPOunBG9eNH1ydcPYxQSTIxwBKQkI/zyNoiy/5J5UYKndJ3Hudb +aQnHGCwkQJ4VpJ33YkhJdV2pP89rHsdxmemdlPHB8G09+Fld4eoI0W5j+utdGxhKYQIDAQAB# +MIICyTBDBgkqhkiG9w0BBQ0wNjAeBgkqhkiG9w0BBQwwEQQIeCiY2t9IweYCAggAAgEYMBQG +CCqGSIb3DQMHBAjyozzRlfQzzwSCAoCaoSHdkWWQNVO0+g6bBKHaAr9rTAQwWkoyMhhmHzlI +DppORiCGtr6Eu3ipgnGQG12itWzvTNXflbga9mTgtFGR2LlJ6SFuts+t6itzd/BAhfpDwLV4 +lvFsdKpEUZMDTCG/yE5gH0TMrqraIk9rUmKme1bTbMuBdK17RrzlPh3Ud9CfwJ1TlElItGWK +4HgUMo4DuxxWw9L4yCK/uB8FZvtYlfKTKYASj4kXuHjJkhju0vS1OtZF9B42aimjEq+qEQqy +fl9Ol2ECdwQem1wbVT+nHUDoyfLDypNR8JriT3IhulH58/twAzasmCb0fgwo2OMbZAERgL6D +iTVpm0gmscBxw7eqaWFt9wrjs+B83Fho8H/UC2dsTCkHhCbIKknE/AG0uX5GXZ5QPfFd53c4 +fo1KcQqMJxEDk7QWiT/WspE/3OWr0SAVVBIm8qhR/JpCYvQ+Pn2dkek0iK8Ke6hixfDY89BR +5UnsCixSsxZqAhbTh47T2BaZFGHRDyQXqj4l5qMgJ2SfAv+qWIoeJ+9dPfQYITeIKsRzCJ2R +F89qDWgk2ed9TwSLF6roo5dSYl6uUXIuemnmpkpuKMdNTIdLFusYfxIQ4KXPqeQmr6vUDnEK +uEnMlGZQa/bNP+XuYl+4g9YZRKTC68oMxN7SsNnTMjwVsYV1hIBDfGIBaS+pemSJ5Kckknu+ +bq9t2yDMts8TasivwrlN8zFXzEVlM0bqsQUdn//lKqiocUciq18L1PSv/VwiyXQcA4tGg/YP +zj94iK9k6eKcFMXGZVAxR+GGn2JKcL4lO60fp59n8EXE2MBzD5MkT1tAEx3jn2VbmAh6m50C +hSq3zxT9RAa7uHQ3VbBv +[end] ============================================================ --- skel/monotonerc e06142b15f04777fd4ef76fc52ab6586f6a87807 +++ skel/monotonerc e06142b15f04777fd4ef76fc52ab6586f6a87807 @@ -0,0 +1,51 @@ +function read_config() + local permfile = io.open("CONFDIR/hostconfig", "r") + if (permfile == nil) then return false end + local dat = permfile:read("*a") + io.close(permfile) + local res = parse_basic_io(dat) + if res == nil then + io.stderr:write("config file cannot be parsed\n") + return false + end + for i, item in pairs(res) + do + if item.name == "hostkey" then + for j, val in pairs(item.values) do + hostkey = val + end + elseif item.name == "hostkeypass" then + for j, val in pairs(item.values) do + hostkeypass = val + end + end + end + return false +end + +read_config(); + +function get_passphrase(keyid) + if keyid == hostkey then + return hostkeypass + end + return nil +end + +function persist_phrase_ok() + return true +end + +old_get_wperm = get_netsync_write_permitted +function get_netsync_write_permitted(ident) + if ident == hostkey then return true end + return old_get_wperm(ident) +end + +function get_netsync_read_permitted(foo, bar) + return true +end + +function note_netsync_end() + execute("DBROTATE", "queue", get_confdir()) +end ============================================================ --- usher/AUTHORS da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ usher/AUTHORS da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- usher/COPYING da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ usher/COPYING da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- usher/ChangeLog da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ usher/ChangeLog da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- usher/INSTALL da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ usher/INSTALL da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- usher/Makefile.am 8279856a25e147ea67621157109f9f7e03900e73 +++ usher/Makefile.am 8279856a25e147ea67621157109f9f7e03900e73 @@ -0,0 +1,21 @@ + +bin_PROGRAMS = usher + +usher_SOURCES = administrator.cc administrator.hh \ + basic_io_serverlist_reader.cc basic_io_serverlist_reader.hh \ + buffer.cc buffer.hh \ + channel.cc channel.hh \ + dir_serverlist_reader.cc dir_serverlist_reader.hh \ + err.cc err.hh \ + io.cc io.hh \ + query_client.cc query_client.hh \ + server.cc server.hh \ + serverlist_reader.cc serverlist_reader.hh \ + server_manager.cc server_manager.hh \ + sock.cc sock.hh \ + usher.cc + +EXTRA_DIST = ChangeLog.txt + +ChangeLog.txt: + $(srcdir)/ChangeLog > $@ ============================================================ --- usher/NEWS da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ usher/NEWS da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- usher/README dfd06ce97cbe36c3af669ff2ec827a5e42d6bb76 +++ usher/README dfd06ce97cbe36c3af669ff2ec827a5e42d6bb76 @@ -0,0 +1,203 @@ +Introduction +------------ +This is an 'usher' to allow multiple monotone servers to work from +the same port. It asks the client what it wants to sync, +and then looks up the matching server in a table. It then forwards +the connection to the specific instance belonging to that server. +All servers using the same usher need to have the same server key. + +Usher requires cooperation from the client, which means it only works +for recent (0.23 or later) clients. In order to match against hostnames +a post-0.23 client is needed (0.23 clients can only be matched against +their include pattern). + +Usage: + usher [-p pidfile] + +Options: + -p pidfile a path to a file (deleted on program exit) to record the pid of the usher in + [optional] + path to a file that which specifies how to map the clients requests + to monotone servers. + [required] + +An 'usher' is useful in at least the following situations: +- a monotone webhosting service where many instances of different databases + are (sometimes) active +- if you have a clear separation of what goes into certain databases, either + based on certain branchnames or hostnames (main in one database, plugins in another etc.) +- if you want to limit the 'namespace' of branchnames in one database + + +Config file syntax +------------------ +The configuration file for usher is in 'basic_io' like formate, similare to +what monotone uses. + +It consists of lines with the following general syntax: + +keyword "value" "value2" "value3" ... + +* values are separated by spaces and enclosed by double quotes. +* blocks of lines are separated by empty lines + +Example: + +userpass "username" "password" +monotone "mtn" "-k" "my_key" +listenaddr "0.0.0.0:4691" +adminaddr "127.0.0.1:12345" +logdir "/var/log/usher/" + +server "monotone" +host "localhost" +pattern "net.venge.monotone" +remote "66.96.28.3:4691" + +server "local" +host "127.0.0.1" +pattern "*" +local "-d" "/usr/local/src/managed/my.db" "*" + +Description of keywords and their values +---------------------------------------- +username "" "" + This defines one or more usernames identified by the password specified + which can access the 'usher' over the administrative port. (see below: Administrative interface) + Insert such a line for every username you need. + Default: none + +monotone "" "" " ... + Describes the arguments usher should use when spawning dynamic monotone + servers. Since usher spawns monotone instances which 'serve' things that + argument is applied + Default: monotone "mtn" + Example: monotone "mtn" "-k" "address@hidden" + +adminaddr "
:" + The administrative (ip-)address and port, where administrative commands + can be issued. (see below: Administrative interface) + Default: none + +listenaddr "
:" + Normal address and portnumber on which the usher process should listen for + incoming connections + Default: listenaddr "0.0.0.0:4691" + +logdir "/path/for/logfiles/" + A path (ending in a slash) where logfiles usher produces should be stored. + The names of the logfiles are taken from the server stanzas in the config + file (described below) appended with '.log' + Default: logdir "./" + +The remainder of the config file consists of stanza's specifying which servers +are running in coordination with usher and how request should be redirected to +them. + +A server block can have two general forms, one for local (dynamically created) +'serve' processes and one for 'remote' monotone processes: + +server "" +host "" +pattern "" +remote "" + +or + +server "" +host "" +pattern "" +local "" "" "" ..."" + + +When usher receives a request (on 'listenaddr'), a match is tried with the +value specified on the 'host' keyword lines. If the initial substring ('stem') +of the requested host matches a value the request is: + +- directed to the host at 'ipaddress:port' if the last line of the block is +'remote'. [: I could not get this to work at all?] +- directed to a dynamically created monontone serving process if the last keyword +line specifies 'local', using the arguments from that line, including the common +arguments specified on the 'monotone' keyword line. + +Note that 'host' has to be an initial substring of what the client asked to +connect to, but does not have to match exactly. This means that you don't have +to know in advance whether clients will be asking for 'host' or 'host:port'. + +If no server block with a matching 'host' is found, the include pattern +offered by the client will be compared against the 'pattern' stem values lines +in the same way. In particular, note that the pattern lines are not globs; +they are initial substrings (stem's) just like the 'host' lines. + +For 'local' servers usher uses a port range (currently hardcoded) to start +monotone serve processes on. The default range is: 15000-65000. Also, for local +serve processes there is an 'idle_timeout' value after which local servers are +terminated if no client is using it. This value is by default 60 seconds. (hardcoded) +[ is this at all affecting performance, wrt cache?] +Similarly, usher will use the local address interface for the address range. +Current range is: 127.0.1.1 - 127.254.254.254 + +Administrative interface +------------------------ +If the 'adminaddr' keyword line is given in the config file, the usher will +listen for administrative connections on the specified address:port. The connecting +client can issue commands of the form (note the lack of quotes here) + +COMMAND [arguments] + +and after any command except USERPASS the usher will send a reply and +close the connection. The reply will always end with a newline. + +USERPASS username password + Required before any other command, so random people can't do bad things. + If incorrect, the connection will be closed immediately. + +STATUS [servername] + Get the status of a server, as named by the "server" lines in the + config file. If a server is specified, the result will be one of: + REMOTE - this is a remote server without active connections + ACTIVE n - this server currently has n active connections + WAITING - this (local) server is running, but has no connections + SLEEPING - this (local) server is not running, but is available + STOPPING n - this (local) server has been asked to stop, but still has + n active connections. It will not accept further connections. + STOPPED - this (local) server has been stopped, and will not accept + connections. The server process is not running. + SHUTTINGDOWN - the usher has been shut down, no servers are accepting + connections. + SHUTDOWN - the usher has been shut down, all connections have been closed, + and all local server processes have been stopped. + +If no server is specified, the repsonse will be SHUTTINGDOWN, SHUTDOWN, +WAITING, or ACTIVE (with n being the total number of open connections, +across all servers). + +STOP servername + Prevent the given local server from receiving further connections, and stop + it once all connections are closed. The result will be the new status of + that server: ACTIVE local servers become STOPPING, and WAITING and SLEEPING + servers become STOPPED. Servers in other states are not affected. + +START servername + Allow a stopped or stopping server to receive connections again. The result + will be the new status of that server. (A server in the "STOPPING" state + becomes ACTIVE, and a STOPPED server becomes SLEEPING. A server in some + other state is not affected.) + +LIST [state] + Returns a space-separated list of all servers. If a state is given, only + list the servers that are in that state. + +SHUTDOWN + Do not accept new connections for any servers, local or remote. Returns "ok". + +STARTUP + Begin accepting connections again after a SHUTDOWN. Returns "ok". + +CONNECTIONS + Returns the number of connections currently open. + +RELOAD + Reload the config file (same as sending SIGHUP). The reply will be "ok", + and will not be given until the config file has been reloaded. + ============================================================ --- usher/administrator.cc 774eb6e37ccf5a2a2e6f4721198e02874dc353e9 +++ usher/administrator.cc 774eb6e37ccf5a2a2e6f4721198e02874dc353e9 @@ -0,0 +1,323 @@ +#include "administrator.hh" +#include "err.hh" +#include "io.hh" + +#include +#include +using std::cerr; +#include +using std::make_pair; +using std::max; +#include +using std::ifstream; +#include +using std::string; +#include +using std::vector; +#include +using std::map; + +#include +using boost::lexical_cast; + +#include + +namespace defaults +{ + string const listenport = "0.0.0.0:4691"; + vector operator%(vector l, string const &r) + { + l.push_back(r); + return l; + } + map > get_defaults() + { + map > out; + out["monotone"] = vector() % "mtn"; + out["logdir"] = vector() % "./"; + return out; + } + map > defaults = get_defaults(); +} + +administrator::cstate::cstate() + : auth(false), rdone(false) +{ +} + +administrator::administrator(server_manager &sm, string const &cf) + : port(-1), serverport(-1), manager(sm), conffile(cf) +{ + reload_conffile(); +} + +bool +administrator::process(cstate & cs) +{ + unsigned int n = cs.buf.find("\n"); + if (n == cs.buf.npos) + return true; + string l = cs.buf.substr(0, n); + cs.buf.erase(0, n+1); + std::istringstream iss(l); + string cmd; + iss>>cmd; + if (cmd == "USERPASS") { + string user, pass; + iss>>user>>pass; + map::iterator i = admins.find(user); + if (i == admins.end() || i->second != pass) { + cerr<<"Failed admin login.\n"; + return false; + } else { + if (cs.auth == true) + return false; + cs.auth = true; + return process(cs); + } + } else if (!cs.auth) { + cs.buf = "You must log in first.\n"; + } else if (cmd == "STATUS") { + string srv; + iss>>srv; + std::ostringstream oss; + if (srv.empty()) { + serverstate ss; + ss.num = manager.get_total_connections(); + if (manager.get_connections_allowed()) { + if (ss.num) + ss.state = serverstate::active; + else + ss.state = serverstate::waiting; + } else { + if (ss.num) + ss.state = serverstate::shuttingdown; + else + ss.state = serverstate::shutdown; + } + oss<>srv; + std::ostringstream oss; + try + { + manager.start_stop_server(srv, true); + } + catch (errstr &e) + { + oss<>srv; + std::ostringstream oss; + try + { + manager.start_stop_server(srv, false); + } + catch (errstr &e) + { + oss<>state; + set servers = manager.list_servers(state); + for (set::iterator i = servers.begin(); + i != servers.end(); ++i) + { + cs.buf += (cs.buf.empty()?"":" ") + *i; + } + cs.buf += "\n"; + } else if (cmd == "SHUTDOWN") { + manager.allow_connections(false); + manager.kill_old_servers(); + cs.buf = "ok\n"; + } else if (cmd == "CONNECTIONS") { + cs.buf = lexical_cast(manager.get_total_connections()) + "\n"; + } else if (cmd == "RELOAD") { + reload_conffile(); + cs.buf = "ok\n"; + } else if (cmd == "STARTUP") { + manager.allow_connections(true); + cs.buf = "ok\n"; + } else { + cs.buf = "unknown command\n"; + } + cs.rdone = true; + return true; +} + +void +administrator::initialize(string const & ap, string const & sp) +{ + sock oldport(-1); + if (!ap.empty()) + { + oldport = port; + try { + port = start(ap); + } catch (errstr & s) { + cerr<<"Could not initialize admin port: "< >::iterator i = conns.begin(); + i != conns.end(); ++i) { + int c = i->second; + if (!i->first.rdone) + FD_SET(c, &rd); + else + FD_SET(c, &wr); + maxfd = max(maxfd, int(c)); + } +} + +void +administrator::process_selected(fd_set & rd, fd_set & wr, fd_set & er) +{ + if (int(port) == -1) + return; + if (FD_ISSET(port, &rd)) { + try { + struct sockaddr_in addr; + unsigned int l = sizeof(addr); + memset(&addr, 0, l); + sock nc = tosserr(accept(port, (struct sockaddr *) + &addr, &l), "accept()"); + conns.push_back(make_pair(cstate(), nc)); + } catch(errstr & s) { + cerr<<"During new admin connection: "< >::iterator> del; + for (list >::iterator i = conns.begin(); + i != conns.end(); ++i) { + int c = i->second; + if (c <= 0) { +// cerr<<"Bad socket.\n"; + del.push_back(i); + } else if (FD_ISSET(c, &rd)) { + char buf[120]; + int n; + n = read(c, buf, 120); + if (n < 1) { +// cerr<<"Read failed.\n"; + del.push_back(i); + } + i->first.buf.append(buf, n); + if (!process(i->first)) { +// cerr<<"Closing connection...\n"; +// i->second.close(); + del.push_back(i); + } + } + else if (FD_ISSET(c, &wr)) { + int n = write(c, i->first.buf.c_str(), i->first.buf.size()); + if (n < 1) { +// cerr<<"Write failed.\n"; + del.push_back(i); + } else { + i->first.buf.erase(0, n); + if (i->first.buf.empty() && i->first.rdone) { +// cerr<<"Done.\n"; + del.push_back(i); + } + } + } + } + for (list >::iterator>::iterator i = del.begin(); + i != del.end(); ++i) { + conns.erase(*i); + } +} + +void +administrator::reload_conffile() +{ + std::ifstream ifs(conffile.c_str()); + vector delims; + delims.push_back("server"); + basic_io::stanza_reader reader(ifs, delims); + basic_io::stanza st; + bool ok = reader.get(st); + if (!ok) + return; + map > opts = defaults::defaults; + admins.clear(); + string ap, sp; + if (serverport == -1) + sp = defaults::listenport; + for (vector::iterator i = st.items.begin(); + i != st.items.end(); ++i) + { + if (i->key == "userpass") + { + if (i->values.size() == 2) + admins.insert(make_pair(i->values[0].parsed, i->values[1].parsed)); + } + else if (i->key == "adminaddr") + { + if (!i->values.empty()) + ap = i->values[0].parsed; + } + else if (i->key == "listenaddr") + { + if (!i->values.empty()) + sp = i->values[0].parsed; + } + else + { + vector vals; + for (std::vector::iterator j = i->values.begin(); + j != i->values.end(); ++j) + { + vals.push_back(j->parsed); + } + opts[i->key] = vals; + } + } + initialize(ap, sp); + manager.set_opts(opts); + manager.reload_servers(); +} ============================================================ --- usher/administrator.hh 2e5057dacae5de057174d7424c7b572ab1c72258 +++ usher/administrator.hh 2e5057dacae5de057174d7424c7b572ab1c72258 @@ -0,0 +1,41 @@ +#ifndef __ADMINISTRATOR_HH_ +#define __ADMINISTRATOR_HH_ + +#include "sock.hh" +#include "server_manager.hh" + +#include +using std::list; +#include +using std::pair; +#include +using std::string; +#include +using std::map; + +#include + +struct administrator +{ + sock port; + sock serverport; + struct cstate + { + bool auth; + bool rdone; + string buf; + cstate(); + }; + list > conns; + map admins; + server_manager &manager; + string conffile; + administrator(server_manager &sm, string const &cf); + bool process(cstate & cs); + void initialize(string const & ap, string const & sp); + void add_to_select(int & maxfd, fd_set & rd, fd_set & wr, fd_set & er); + void process_selected(fd_set & rd, fd_set & wr, fd_set & er); + void reload_conffile(); +}; + +#endif ============================================================ --- usher/basic_io_serverlist_reader.cc 1222bec508bf6134ef5d15165af8e0ebbde1bde3 +++ usher/basic_io_serverlist_reader.cc 1222bec508bf6134ef5d15165af8e0ebbde1bde3 @@ -0,0 +1,119 @@ +#include "basic_io_serverlist_reader.hh" + +#include + +using std::string; +using std::vector; +using std::set; + +using namespace basic_io; + +basic_io_serverlist_reader::basic_io_serverlist_reader(string const &fn) + : filename(fn) +{ +} + +basic_io_serverlist_reader::~basic_io_serverlist_reader() +{ +} + +serverspec +basic_io_serverlist_reader::get_next() +{ + serverspec ss; + if (!reader) + { + ss.valid = false; + return ss; + } + basic_io::stanza st; + bool ok = reader->get(st); + if (st.items.empty()) + ok = false; + else if (st.items[0].key != "server") + { + return get_next(); + } + ss.valid = ok; + if (!ok) + { + return ss; + } + for (vector::iterator i = st.items.begin(); + i != st.items.end(); ++i) + { + if (i->key == "server") + { + if (i->values.empty()) + { + return get_next(); + } + ss.name = i->values[0].parsed; + } + else if (i->key == "pattern") + { + for (vector::iterator j = i->values.begin(); + j != i->values.end(); ++j) + { + ss.patterns.insert(j->parsed); + } + } + else if (i->key == "host") + { + for (vector::iterator j = i->values.begin(); + j != i->values.end(); ++j) + { + ss.hosts.insert(j->parsed); + } + } + else if (i->key == "remote") + { + ss.local = false; + if (i->values.empty()) + { + return get_next(); + } + string str = i->values[0].parsed; + unsigned int c = str.find(":"); + ss.remote_host = str.substr(0, c); + if (c != str.npos) + ss.remote_port = boost::lexical_cast(str.substr(c+1)); + } + else if (i->key == "local") + { + ss.local = true; + for (vector::iterator j = i->values.begin(); + j != i->values.end(); ++j) + { + ss.local_args.push_back(j->parsed); + } + } + else + { + return get_next(); + } + } + return ss; +} + +bool +basic_io_serverlist_reader::begin_reading() +{ + if (ifs.is_open()) + ifs.close(); + ifs.open(filename.c_str()); + if (!ifs) + { + } + vector delims; + delims.push_back("server"); + reader.reset(new basic_io::stanza_reader(ifs, delims)); + return ifs; +} + +void +basic_io_serverlist_reader::end_reading() +{ + reader.reset(); + ifs.close(); +} ============================================================ --- usher/basic_io_serverlist_reader.hh 19f9f608cf418f7662b957a68649771294711bac +++ usher/basic_io_serverlist_reader.hh 19f9f608cf418f7662b957a68649771294711bac @@ -0,0 +1,23 @@ +#ifndef __BASIC_IO_SERVERLIST_READER_HH_ +#define __BASIC_IO_SERVERLIST_READER_HH_ + +#include "serverlist_reader.hh" +#include "io.hh" + +#include +#include +#include + +struct basic_io_serverlist_reader : public serverlist_reader +{ + std::string filename; + std::ifstream ifs; + boost::scoped_ptr reader; + basic_io_serverlist_reader(std::string const &fn); + ~basic_io_serverlist_reader(); + virtual serverspec get_next(); + virtual bool begin_reading(); + virtual void end_reading(); +}; + +#endif ============================================================ --- usher/bootstrap 59bc8c05eea55a0dfafe641e8c976315b3da23bc +++ usher/bootstrap 59bc8c05eea55a0dfafe641e8c976315b3da23bc @@ -0,0 +1,2 @@ +#!/bin/sh +AUTOMAKE=automake-1.7 ACLOCAL=aclocal-1.7 autoreconf --install ============================================================ --- usher/buffer.cc 430ce3374bb6938b832e57e9636a8cfcbe67d0b9 +++ usher/buffer.cc 430ce3374bb6938b832e57e9636a8cfcbe67d0b9 @@ -0,0 +1,71 @@ +#include "buffer.hh" +#include "err.hh" + +#include + +buffer::buffer() + : readpos(0), writepos(0) +{ + ptr = new char[buf_size]; +} + +buffer::buffer(buffer const & b) +{ + ptr = new char[buf_size]; + memcpy(ptr, b.ptr, buf_size); + readpos = b.readpos; + writepos = b.writepos; +} + +buffer::~buffer() +{ + delete[] ptr; +} + +bool +buffer::canread() +{ + return writepos > readpos; +} + +bool +buffer::canwrite() +{ + return writepos < buf_size; +} + +void +buffer::getread(char *& p, int & n) +{ + p = ptr + readpos; + n = writepos - readpos; +} + +void +buffer::getwrite(char *& p, int & n) +{ + p = ptr + writepos; + n = buf_size - writepos; +} + +void +buffer::fixread(int n) +{ + if (n < 0) throw errstr("negative read\n", 0); + readpos += n; + if (readpos == writepos) { + readpos = writepos = 0; + } else if (readpos > buf_reset_size) { + memcpy(ptr, ptr+readpos, writepos-readpos); + writepos -= readpos; + readpos = 0; + } +} + +void +buffer::fixwrite(int n) +{ + if (n < 0) + throw errstr("negative write\n", 0); + writepos += n; +} ============================================================ --- usher/buffer.hh 4c747ee60d3e481cd03b17a4dff0d75a1dc7f474 +++ usher/buffer.hh 4c747ee60d3e481cd03b17a4dff0d75a1dc7f474 @@ -0,0 +1,22 @@ +#ifndef __BUFFER_HH_ +#define __BUFFER_HH_ + +struct buffer +{ + static int const buf_size = 2048; + static int const buf_reset_size = 1024; + char * ptr; + int readpos; + int writepos; + buffer(); + ~buffer(); + buffer(buffer const & b); + bool canread(); + bool canwrite(); + void getread(char *& p, int & n); + void getwrite(char *& p, int & n); + void fixread(int n); + void fixwrite(int n); +}; + +#endif ============================================================ --- usher/channel.cc 52d8c79fb85d032b9e89cd6e5aa2413fad04f084 +++ usher/channel.cc 52d8c79fb85d032b9e89cd6e5aa2413fad04f084 @@ -0,0 +1,140 @@ +#include "channel.hh" +#include "query_client.hh" +#include "server_manager.hh" +#include "err.hh" + +#include +using std::max; +#include + +#include +#include + +channel::channel(sock & c, server_manager &sm) + : num(++counter), + cli(c), srv(-1), + have_routed(false), no_server(false), + manager(sm) +{ + char * dat; + int size; + make_packet(greeting, dat, size); + char *p; + int n; + sbuf.getwrite(p, n); + if (n < size) size = n; + memcpy(p, dat, size); + sbuf.fixwrite(size); + delete[] dat; + + cli.write_from(sbuf); +} + +channel::~channel() +{ + if (srv && !no_server) + manager.disconnect_from_server(srv); +} + +bool +channel::is_finished() +{ + return (cli == -1) && (srv == -1); +} + +void +channel::add_to_select(int & maxfd, fd_set & rd, fd_set & wr, fd_set & er) +{ + int c = cli; + int s = srv; + + if (c > 0) { + FD_SET(c, &er); + if (cbuf.canwrite()) + FD_SET(c, &rd); + if (sbuf.canread()) + FD_SET(c, &wr); + maxfd = max(maxfd, c); + } + if (s > 0) { + FD_SET(s, &er); + if (sbuf.canwrite()) + FD_SET(s, &rd); + if (cbuf.canread()) + FD_SET(s, &wr); + maxfd = max(maxfd, s); + } +} + +bool +channel::process_selected(fd_set & rd, fd_set & wr, fd_set & er) +{ + int c = cli; + int s = srv; +/* NB: read oob data before normal reads */ + if (c > 0 && FD_ISSET(c, &er)) { + char d; + errno = 0; + if (recv(c, &d, 1, MSG_OOB) < 1) + cli.close(), c = -1; + else + send(s, &d, 1, MSG_OOB); + } + if (s > 0 && FD_ISSET(s, &er)) { + char d; + errno = 0; + if (recv(s, &d, 1, MSG_OOB) < 1) + srv.close(), s = -1; + else + send(c, &d, 1, MSG_OOB); + } + + char *p=0; + int n; + + if (c > 0 && FD_ISSET(c, &rd)) { + if (!cli.read_to(cbuf)) c = -1; + if (!have_routed) { + string reply_srv, reply_pat; + if (extract_reply(cbuf, reply_srv, reply_pat)) { + try { + serversock ss = manager.connect_to_server(reply_srv, reply_pat); + srv = ss; + have_routed = true; + s = srv; + } catch (errstr & e) { + char * dat; + int size; + sbuf.getwrite(p, n); + make_packet("!" + e.name, dat, size); + if (n < size) size = n; + memcpy(p, dat, size); + sbuf.fixwrite(size); + delete[] dat; + no_server = true; + } + } + } + } + if (s > 0 && FD_ISSET(s, &rd)) { + if (!srv.read_to(sbuf)) s = -1; + } + + if (c > 0 && FD_ISSET(c, &wr)) { + if (!cli.write_from(sbuf)) c = -1; + } + if (s > 0 && FD_ISSET(s, &wr)) { + if (!srv.write_from(cbuf)) s = -1; + } + + // close sockets we have nothing more to send to + if (c < 0 && !cbuf.canread()) { + srv.close(), s = -1; + } + if ((no_server || have_routed && s < 0) && !sbuf.canread()) { + cli.close(), c = -1; + } + return true; +} + +int channel::counter = 0; ============================================================ --- usher/channel.hh e7c35bd545765f9aa0908a1b955132bb3ba69802 +++ usher/channel.hh e7c35bd545765f9aa0908a1b955132bb3ba69802 @@ -0,0 +1,33 @@ +#ifndef __CHANNEL_HH_ +#define __CHANNEL_HH_ + +#include "sock.hh" +#include "buffer.hh" +#include "server_manager.hh" + +#include +using boost::shared_ptr; + +#include + +struct server_manager; + +struct channel +{ + static int counter; + int num; + sock cli; + serversock srv; + bool have_routed; + bool no_server; + buffer cbuf; + buffer sbuf; + server_manager &manager; + channel(sock & c, server_manager &sm); + ~channel(); + bool is_finished(); + void add_to_select(int & maxfd, fd_set & rd, fd_set & wr, fd_set & er); + bool process_selected(fd_set & rd, fd_set & wr, fd_set & er); +}; + +#endif ============================================================ --- usher/configure.in a191b18290b0903b951d62b95ddd9a4f8e769e0b +++ usher/configure.in a191b18290b0903b951d62b95ddd9a4f8e769e0b @@ -0,0 +1,30 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ(2.59) +AC_INIT(usher, 0.1, BUG-REPORT-ADDRESS) +AM_INIT_AUTOMAKE + +AC_CONFIG_SRCDIR([usher.cc]) +AC_CONFIG_HEADER([config.h]) + +# Checks for programs. +AC_PROG_CXX + +# Checks for libraries. + +# Checks for header files. +AC_HEADER_SYS_WAIT +AC_CHECK_HEADERS([unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_HEADER_STDBOOL +AC_C_CONST +AC_C_INLINE +AC_TYPE_PID_T + +# Checks for library functions. +AC_FUNC_FORK +AC_HEADER_STDC +AC_CHECK_FUNCS([dup2]) +AC_OUTPUT(Makefile) ============================================================ --- usher/dir_serverlist_reader.cc eb07a697b97f3580bb5c5ffbf11a27ba250b6cfa +++ usher/dir_serverlist_reader.cc eb07a697b97f3580bb5c5ffbf11a27ba250b6cfa @@ -0,0 +1,88 @@ +#include "dir_serverlist_reader.hh" + +#include + +using std::string; +using std::vector; +using std::set; + +using namespace basic_io; + +dir_serverlist_reader::dir_serverlist_reader(string const &fn) + : filename(fn) +{ +} + +dir_serverlist_reader::~dir_serverlist_reader() +{ +} + +serverspec +dir_serverlist_reader::get_next() +{ + serverspec ss; + if (!dir) + { + ss.valid = false; + return ss; + } + dirent *d = readdir(dir); + if (!d) + { + ss.valid = false; + return ss; + } + if (!strncmp(d->d_name, ".", 2) || !strncmp(d->d_name, "..", 3)) + return get_next(); + ss.name = d->d_name; + ss.valid = true; + ss.local = true; + string mydir = project_dir + "/" + ss.name; + ss.local_args.push_back("--db=" + mydir + "/database"); + ss.local_args.push_back("--confdir=" + mydir); + ss.local_args.push_back("*"); + ss.hosts.insert(ss.name + "."); + return ss; +} + +bool +dir_serverlist_reader::begin_reading() +{ + std::ifstream ifs(filename.c_str()); + vector delims; + basic_io::stanza_reader reader(ifs, delims); + basic_io::stanza st; + bool ok = reader.get(st); + ifs.close(); + if (!ok) + return false; + + for (vector::iterator i = st.items.begin(); + i != st.items.end(); ++i) + { + if (i->key == "project_dir") + { + if (!i->values.empty()) + project_dir = i->values[0].parsed; + } + else if (i->key == "monotone") + { + if (!i->values.empty()) + monotone = i->values[0].parsed; + } + else if (i->key == "logdir") + { + if (!i->values.empty()) + logdir = i->values[0].parsed; + } + } + dir = opendir(project_dir.c_str()); + + return dir; +} + +void +dir_serverlist_reader::end_reading() +{ + closedir(dir); +} ============================================================ --- usher/dir_serverlist_reader.hh 8ce0af271ee1f8e8e474b5ed7689a420c342342e +++ usher/dir_serverlist_reader.hh 8ce0af271ee1f8e8e474b5ed7689a420c342342e @@ -0,0 +1,24 @@ +#ifndef __DIR_SERVERLIST_READER_HH_ +#define __DIR_SERVERLIST_READER_HH_ + +#include "serverlist_reader.hh" +#include "io.hh" + +#include + +#include +#include + +struct dir_serverlist_reader : public serverlist_reader +{ + std::string filename; + DIR *dir; + std::string project_dir, monotone, logdir; + dir_serverlist_reader(std::string const &fn); + ~dir_serverlist_reader(); + virtual serverspec get_next(); + virtual bool begin_reading(); + virtual void end_reading(); +}; + +#endif ============================================================ --- usher/err.cc 2fef2a9feb7078a0f30ed64c6a1789ddfca7f5bb +++ usher/err.cc 2fef2a9feb7078a0f30ed64c6a1789ddfca7f5bb @@ -0,0 +1,22 @@ +#include "err.hh" + +#include + +errstr::errstr(std::string const & s) + : name(s), err(0) +{ +} + +errstr::errstr(std::string const & s, int e) + : name(s), err(e) +{ +} + +int tosserr(int ret, std::string const & name) +{ + if (ret == -1) + throw errstr(name, errno); + if (ret < 0) + throw errstr(name, ret); + return ret; +} ============================================================ --- usher/err.hh 7880a96e6e31043b0079d2641107f20afbbcb016 +++ usher/err.hh 7880a96e6e31043b0079d2641107f20afbbcb016 @@ -0,0 +1,16 @@ +#ifndef __ERR_HH_ +#define __ERR_HH_ + +#include + +struct errstr +{ + std::string name; + int err; + errstr(std::string const & s); + errstr(std::string const & s, int e); +}; + +int tosserr(int ret, std::string const & name); + +#endif ============================================================ --- usher/forked_server_manager.cc 8ae502d0befd209b2b7e31c1e08af8d2cfeda90a +++ usher/forked_server_manager.cc 8ae502d0befd209b2b7e31c1e08af8d2cfeda90a @@ -0,0 +1,63 @@ +#include "forked_server_manager.hh" +#include "serverlist_reader.hh" +#include "err.hh" + +#include +using std::max; +using std::min; +using std::make_pair; + +#include +#include + +forked_server_manager::forked_server_manager(serverlist_reader &r) + : server_manager(r) +{ + int p[2]; + pipe(p); + from_main = p[1]; + to_thread = p[0]; + pipe(p); + from_thread = p[1]; + to_main = p[0]; + pid = fork(); + if (!pid) + { + exit(0); + } +} + +void +forked_server_manager::add_replace_server(shared_ptr srv, + serverdata const &dat) +{ +} + +void +forked_server_manager::delist_server(shared_ptr srv) +{ +} + +void +forked_server_manager::reload_servers() +{ +} + +serversock +forked_server_manager::connect_to_server(string const &host, + string const &pattern) +{ +} +void +forked_server_manager::disconnect_from_server(serversock const &s) +{ +} +serverstate +forked_server_manager::get_server_state(string const &name) +{ +} + +void +forked_server_manager::kill_old_servers() +{ +} ============================================================ --- usher/forked_server_manager.hh 220b035b43a7f62fa99938d23ab7dc8b2f4ba3fd +++ usher/forked_server_manager.hh 220b035b43a7f62fa99938d23ab7dc8b2f4ba3fd @@ -0,0 +1,19 @@ +#ifndef __SERVER_MANAGER_HH_ +#define __SERVER_MANAGER_HH_ + +#include "server_manager.hh" + +struct forked_server_manager : public server_manager +{ + int pid; + int to_main, from_main, to_thread, from_thread; + forked_server_manager(serverlist_reader &r); + + void reload_servers(); + serversock connect_to_server(string const &host, string const &pattern); + void disconnect_from_server(serversock const &s); + serverstate get_server_state(string const &name); + void kill_old_servers(); +}; + +#endif ============================================================ --- usher/io.cc b0753ee961d8799ad24f008d6497132fc07ad9c2 +++ usher/io.cc b0753ee961d8799ad24f008d6497132fc07ad9c2 @@ -0,0 +1,157 @@ +// 2006-04-13 Timothy Brownawell +// +// The basic_io implementation in monotone represents basic_io as a +// token stream. This represents it as a data structure. +// Also, this allows i/o to/from iostreams, not just strings. + +#include "io.hh" + + +using namespace basic_io; + +istream &basic_io::operator>>(istream &in, value &v) +{ + v.literal.clear(); + v.parsed.clear(); + char c; + in.get(c); + if (c == '[') + { + v.literal = c; + while (in.get(c) && std::isxdigit(c)) + { + v.literal += c; + v.parsed += c; + } + if (c == ']') + v.literal += c; + else + in.setstate(std::ios_base::failbit);//parse error + } + else if (c == '"') + { + v.literal = c; + while (in.get(c) && c != '"') + { + if (c == '\\') + { + v.literal += c; + in.get(c); + } + v.literal += c; + v.parsed += c; + } + if (c == '"') + v.literal += c; + else + in.setstate(std::ios_base::failbit);//parse error + } + else + { + in.unget(); + } + return in; +} +ostream &basic_io::operator<<(ostream &out, value &v) +{ + out<>(istream &in, item &i) +{ + i.key.clear(); + i.values.clear(); + bool got_key = false, done = false; + char c; + in >> c; + if (!in) + return in; + in.unget(); + do + { + if ((c == '[' || c == '"') && got_key) + { + value v; + in >> v; + i.values.push_back(v); + } + else if (!got_key && c != '[' && c != '"') + { + in >> i.key; + i.longest = i.key.size(); + got_key = true; + } + else + done = true; + bool ok = in; + in >> c; + in.unget(); + if (ok && !in) + { + in.clear(); + done = true; + } + } + while (!done && in); + return in; +} +ostream &basic_io::operator<<(ostream &out, item const &i) +{ + out<::const_iterator j = i.values.begin(); + j != i.values.end(); ++j) + { + out << " " << j->literal; + } + out<<"\n"; + return out; +} + +void +stanza::compute_longest() +{ + unsigned int longest=0; + for (vector::const_iterator i = items.begin(); + i != items.end(); ++i) + { + longest=(i->key.size()>longest?i->key.size():longest); + } + for (vector::iterator i = items.begin(); + i != items.end(); ++i) + { + i->longest = longest; + } +} +ostream &basic_io::operator<<(ostream &out, stanza const &st) +{ + for (vector::const_iterator i = st.items.begin(); + i != st.items.end(); ++i) + { + out << *i; + } + return out; +} +bool +stanza_reader::get(stanza &st) +{ + st.items.clear(); + while ((st.items.empty() && !last_read.key.empty()) || is >> last_read) + { + if (delims.find(last_read.key) == delims.end() || st.items.empty()) + st.items.push_back(last_read); + else + { + st.compute_longest(); + return true; + } + } + if (!is && !st.items.empty()) + { + st.compute_longest(); + return true; + } + return false; +} ============================================================ --- usher/io.hh ccad8be31096e6ca11cae88425b5986ea258f07c +++ usher/io.hh ccad8be31096e6ca11cae88425b5986ea258f07c @@ -0,0 +1,57 @@ +#ifndef __IO_HH_ +#define __IO_HH_ +// 2006-04-13 Timothy Brownawell +// +// The basic_io implementation in monotone represents basic_io as a +// token stream. This represents it as a data structure. +// Also, this allows i/o to/from iostreams, not just strings. + +#include +#include +#include +#include + +namespace basic_io { + using std::string; + using std::vector; + using std::istream; + using std::ostream; + using std::set; + struct value + { + string literal, parsed; + }; + istream &operator>>(istream &in, value &v); + ostream &operator<<(ostream &out, value &v); + struct item + { + string key; + vector values; + int longest; + }; + istream &operator>>(istream &in, item &i); + ostream &operator<<(ostream &out, item const &i); + + struct stanza + { + vector items; + void compute_longest(); + }; + ostream &operator<<(ostream &out, stanza const &st); + // There is no operator>>(istream, stanza), because that operation + // requires knowing what the stanza delimiters are. + // So we have a stanza_reader instead. + struct stanza_reader + { + istream &is; + set delims; + item last_read; + stanza_reader(istream &i, vector const &d) + : is(i), delims(d.begin(), d.end()) + { + } + bool get(stanza &st); + }; +} + +#endif ============================================================ --- usher/query_client.cc 4aaedbdb9403d7cb33f459a7a0b6e500716cc341 +++ usher/query_client.cc 4aaedbdb9403d7cb33f459a7a0b6e500716cc341 @@ -0,0 +1,123 @@ +#include "buffer.hh" +#include "query_client.hh" + +#include +using std::string; +#include +using std::cerr; + +string const greeting = " Hello! This is the monotone usher at localhost. What would you like?"; + +string const notfound = "!Sorry, I don't know where to find that."; + +string const disabled = "!Sorry, this usher is not currently accepting connections."; + +string const srvdisabled = "!Sorry, that server is currently disabled."; + +// packet format is: +// byte version +// byte cmd {100 if we send, 101 if we receive} +// uleb128 {size of everything after this} +// uleb128 {size of string} +// string +// { +// uleb128 {size of string} +// string +// } // only present if we're receiving + +// uleb128 is +// byte 0x80 | +// byte 0x80 | +// ... +// byte 0xff & +// +// the high bit says that this byte is not the last + +int extract_uleb128(char *p, int maxsize, int & out) +{ + out = 0; + int b = 0; + unsigned char got; + do { + if (b == maxsize) + return -1; + got = p[b]; + out += ((int)(p[b] & 0x7f))<<(b*7); + ++b; + } while ((unsigned int)(b*7) < sizeof(int)*8-1 && (got & 0x80)); + return b; +} + +int extract_vstr(char *p, int maxsize, string & out) +{ + int size; + out.clear(); + int chars = extract_uleb128(p, maxsize, size); + if (chars == -1 || chars + size > maxsize) { + return -1; + } + out.append(p+chars, size); + return chars+size; +} + +bool extract_reply(buffer & buf, string & host, string & pat) +{ + char *p; + int n, s; + buf.getread(p, n); + if (n < 4) return false; + p += 2; // first 2 bytes are header + n -= 2; + // extract size, and make sure we have the entire packet + int pos = extract_uleb128(p, n, s); + if (pos == -1 || n < s+pos) { + return false; + } + // extract host vstr + int num = extract_vstr(p+pos, n-pos, host); + if (num == -1) { + return false; + } + pos += num; + // extract pattern vstr + num = extract_vstr(p+pos, n-pos, pat); + if (num == -1) { + cerr<<"old-style reply.\n"; + pat = host; + host.clear(); + return true; + } + pos += num; + buf.fixread(pos+2); + return true; +} + +void make_packet(std::string const & msg, char * & pkt, int & size) +{ + size = msg.size(); + char const * txt = msg.c_str(); + char header[6]; + header[0] = 0; + header[1] = 100; + int headersize; + if (size >= 128) { + header[2] = 0x80 | (0x7f & (char)(size+2)); + header[3] = (char)((size+2)>>7); + header[4] = 0x80 | (0x7f & (char)(size)); + header[5] = (char)(size>>7); + headersize = 6; + } else if (size >= 127) { + header[2] = 0x80 | (0x7f & (char)(size+1)); + header[3] = (char)((size+1)>>7); + header[4] = (char)(size); + headersize = 5; + } else { + header[2] = (char)(size+1); + header[3] = (char)(size); + headersize = 4; + } + pkt = new char[headersize + size]; + memcpy(pkt, header, headersize); + memcpy(pkt + headersize, txt, size); + size += headersize; +} ============================================================ --- usher/query_client.hh e45fb70cd2feee54885cded16c277071ff0b8892 +++ usher/query_client.hh e45fb70cd2feee54885cded16c277071ff0b8892 @@ -0,0 +1,15 @@ +#ifndef __QUERY_CLIENT_HH_ +#define __QUERY_CLIENT_HH_ + +#include + +struct buffer; + +extern std::string const greeting; +extern std::string const disabled; + +void make_packet(std::string const & msg, char * & pkt, int & size); + +bool extract_reply(buffer & buf, std::string & host, std::string & pat); + +#endif ============================================================ --- usher/server.cc dc8713e8c772cd307cecafb7d3c47a63bd92a4e2 +++ usher/server.cc dc8713e8c772cd307cecafb7d3c47a63bd92a4e2 @@ -0,0 +1,357 @@ +#include "server.hh" +#include "err.hh" +#include "server_manager.hh" + +#include +using std::cerr; + +#include +#include +#include +#include +#include +#include + +#include +using boost::lexical_cast; + +// keep local servers around for this many seconds after the last +// client disconnects from them (only accurate to ~10 seconds) +int const server_idle_timeout = 60; + +// ranges that dynamic (local) servers can be put on +int const minport = 15000; +int const maxport = 65000; +int const minaddr[] = {127, 0, 1, 1}; +int const maxaddr[] = {127, 254, 254, 254}; +int currport = 0; +int curraddr[] = {0, 0, 0, 0}; + +int fork_server(string const &logfile, vector const & args); + +serverstate::serverstate(): state(unknown), num(0) {} +serverstate const & serverstate::operator=(string const & s) +{ + if (s == "REMOTE") + state = remote; + else if (s == "ACTIVE") + state = active; + else if (s == "WAITING") + state = waiting; + else if (s == "SLEEPING") + state = sleeping; + else if (s == "STOPPING") + state = stopping; + else if (s == "STOPPED") + state = stopped; + else if (s == "SHUTTINGDOWN") + state = shuttingdown; + else if (s == "SHUTDOWN") + state = shutdown; + return *this; +} +bool serverstate::operator==(string const & s) +{ + serverstate foo; + foo = s; + return foo.state == state; +} + +std::ostream & operator<<(std::ostream & os, serverstate const & ss) +{ + switch (ss.state) { + case serverstate::remote: + os<<"REMOTE"; + break; + case serverstate::active: + os<<"ACTIVE "< +server::self() +{ + return me.lock(); +} + +serverstate +server::get_state() +{ + serverstate ss; + ss.num = connection_count; + if (!manager.get_connections_allowed()) { + if (!manager.get_total_connections()) + ss.state = serverstate::shutdown; + else + ss.state = serverstate::shuttingdown; + } else if (connection_count) { + if (enabled) + ss.state = serverstate::active; + else + ss.state = serverstate::stopping; + } else if (!local) + ss.state = serverstate::remote; + else if (!enabled) + ss.state = serverstate::stopped; + else if (pid == -1) + ss.state = serverstate::sleeping; + else + ss.state = serverstate::waiting; + return ss; +} + +static bool +check_address_empty(string const & addr, int port) +{ + sock s = tosserr(socket(AF_INET, SOCK_STREAM, 0), "socket()"); + int yes = 1; + tosserr(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + &yes, sizeof(yes)), "setsockopt"); + sockaddr_in a; + memset (&a, 0, sizeof (a)); + if (!inet_aton(addr.c_str(), (in_addr *) &a.sin_addr.s_addr)) + throw errstr("bad ip address format", 0); + a.sin_port = htons(port); + a.sin_family = AF_INET; + int r = bind(s, (sockaddr *) &a, sizeof(a)); + s.close(); + return r == 0; +} + +static void +find_addr(string & addr, int & port) +{ + if (currport == 0) { + currport = minport-1; + for(int i = 0; i < 4; ++i) + curraddr[i] = minaddr[i]; + } + do { + // get the next address in our list + if (++currport > maxport) { + currport = minport; + for (int i = 0; i < 4; ++i) { + if (++curraddr[i] <= maxaddr[i]) + break; + curraddr[i] = minaddr[i]; + } + } + port = currport; + addr = lexical_cast(curraddr[0]) + "." + + lexical_cast(curraddr[1]) + "." + + lexical_cast(curraddr[2]) + "." + + lexical_cast(curraddr[3]); + } while (!check_address_empty(addr, port)); +} + +static sock +make_outgoing(int port, string const & address) +{ + sock s = tosserr(socket(AF_INET, SOCK_STREAM, 0), "socket()"); + + struct sockaddr_in a; + memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; + a.sin_port = htons(port); + + if (!inet_aton(address.c_str(), (in_addr *) &a.sin_addr.s_addr)) + throw errstr("bad ip address format", 0); + + tosserr(connect(s, (sockaddr *) &a, sizeof (a)), "connect()"); + return s; +} + +sock +server::connect() +{ + if (!enabled) + throw errstr("This server is disabled."); + map, server_manager::serverdata>::iterator + i = manager.servers.find(self()); + map > opts = manager.get_opts(); + string logfilename; + if (!opts["logdir"].empty()); + { + logfilename = opts["logdir"].front(); + if (!logfilename.empty() && logfilename[logfilename.size()-1] != '/') + logfilename += '/'; + } + logfilename += i->second.name + ".log"; + if (local && pid == -1) { + // server needs to be started + // we'll try 3 times, since there's a delay between our checking that + // a port's available and the server taking it + for (int i = 0; i < 3 && pid == -1; ++i) { + if (i > 0 || port == 0) + find_addr(addr, port); + vector args = opts["monotone"]; + args.push_back("serve"); + args.push_back("--bind=" + addr + ":" + lexical_cast(port)); + for (vector::iterator j = arguments.begin(); + j != arguments.end(); ++j) + args.push_back(*j); + pid = fork_server(logfilename, args); + } + } + if (pid == -1) + throw errstr("Cannot fork server."); + sock s = make_outgoing(port, addr); + ++connection_count; + return s; +} + +void +server::disconnect() +{ + if (--connection_count || !local) + return; + last_conn_time = time(0); + maybekill(); +} + +void +server::maybekill() +{ + if (!local) + return; + if (pid == -1) + return; + int difftime = time(0) - last_conn_time; + if (!connection_count + && (difftime > server_idle_timeout + || !manager.get_connections_allowed())) + yeskill(); + else if (waitpid(pid, 0, WNOHANG) > 0) { + pid = -1; + port = 0; + } +} + +void +server::yeskill() +{ + if (local && pid != -1) { + kill(pid, SIGTERM); + int r; + do {r = waitpid(pid, 0, 0);} while (r==-1 && errno == EINTR); + pid = -1; + port = 0; + } +} + +int fork_server(string const &logfile, vector const & args) +{ + int err[2]; + if ((err[1]=open(logfile.c_str(),O_CREAT|O_APPEND|O_WRONLY,0644)) < 0) + return -1; + if ((err[0]=open(logfile.c_str(),O_RDONLY)) < 0) + { + close(err[1]); + return -1; + } + lseek(err[0], 0, SEEK_END); + int pid = fork(); + if (pid == -1) { + close(err[0]); + close(err[1]); + cerr<<"Failed to fork server.\n"; + return -1; + } else if (pid == 0) { + close(err[0]); + close(0); + close(1); + close(2); + sock::close_all_socks(); + if (dup2(err[1], 1) < 0) { + exit(4); + } + close(err[1]); + if (dup2(1, 2) < 0) { + exit(5); + } + + char ** a = new char*[args.size()+1]; + for (unsigned int i = 0; i < args.size(); ++i) { + a[i] = new char[args[i].size()+1]; + memcpy(a[i], args[i].c_str(), args[i].size()+1); + } + a[args.size()] = 0; + + // Must set C locale, because usher interprets the output from + // monotone itself, and only deals with english! + setlocale(LC_ALL,"C"); + setlocale(LC_MESSAGES,"C"); + setlocale(LC_NUMERIC,"C"); + setenv("LC_ALL","C",1); + + execvp(a[0], a); + perror("execvp failed"); + exit(6); + } else { + close(err[1]); + char head[256]; + int got = 0; + int r = 0, w = 0; + bool line = false; + // the first line output on the server's stderr will be either + // "mtn: beginning service on : " or + // "mtn: network error: bind(2) error: Address already in use" + // + // Now that reads a file instead of a pipe, we need to poll it + // until either we get our output or the server dies. This is + // because read() doesn't block on EOF. + do { + usleep(10*1000); + r = read(err[0], head + got, 256 - got); + if (r) + cerr<<"Read '"< 0) { + for (int i = 0; i < r && !line; ++i) + if (head[got+i] == '\n') + line = true; + got += r; + } + w = waitpid(pid, 0, WNOHANG); + } while((!w || r > 0) && !line && got < 256); + head[got] = 0; + close(err[0]); + if (string(head).find("beginning service") != string::npos) + return pid; + kill(pid, SIGKILL); + int status; + do {r = waitpid(pid, &status, 0);} while (r==-1 && errno == EINTR); + return -1; + } +} ============================================================ --- usher/server.hh 3b06fa45604b1a9aea829006c630a5595e7dff96 +++ usher/server.hh 3b06fa45604b1a9aea829006c630a5595e7dff96 @@ -0,0 +1,52 @@ +#ifndef __SERVER_HH_ +#define __SERVER_HH_ + +#include "sock.hh" + +#include +using std::string; +#include +using std::vector; + +#include +using boost::shared_ptr; +#include +using boost::weak_ptr; + +struct server_manager; + +struct serverstate +{ + enum ss {remote, active, waiting, sleeping, stopping, + stopped, shuttingdown, shutdown, unknown}; + ss state; + int num; + serverstate(); + serverstate const & operator=(string const & s); + bool operator==(string const & s); +}; +std::ostream & operator<<(std::ostream & os, serverstate const & ss); + +struct server +{ + bool enabled; + bool local; + int pid; + vector arguments; + string addr; + int port; + int connection_count; + int last_conn_time; + server_manager &manager; + weak_ptr me; + server(server_manager &sm); + ~server(); + serverstate get_state(); + sock connect(); + void disconnect(); + void maybekill(); + void yeskill(); + shared_ptr self(); +}; + +#endif ============================================================ --- usher/server_manager.cc 36910e7007c355bd549ebd61555afde8e440b59c +++ usher/server_manager.cc 36910e7007c355bd549ebd61555afde8e440b59c @@ -0,0 +1,280 @@ +#include "server_manager.hh" +#include "serverlist_reader.hh" +#include "err.hh" + +#include +using std::max; +using std::min; +using std::make_pair; + +#include + +#include +//#include + +serversock::serversock(sock const &o) + : sock(o) +{ +} + +bool +server_manager::serverdata::operator==(serverdata const &o) +{ + return name == o.name && hosts == o.hosts && patterns == o.patterns; +} + +server_manager::server_manager(serverlist_reader &r) + : connections_allowed(true), reader(r) +{ + reload_servers(); +} + +server_manager::prefix::prefix(string const &s) + : str(s), ok_shorter(true) {} +server_manager::prefix::prefix(string const &s, bool b) + : str(s), ok_shorter(b) {} +server_manager::prefix::~prefix() {} +server_manager::prefix::operator string() {return str;} +int server_manager::prefix::cmp(prefix const &r) const +{ + unsigned int size = min(str.size(), r.str.size()); + int c = strncmp(str.c_str(), r.str.c_str(), size); + int longer = str.size() - r.str.size();// positive is *this is longer than r + int res = c; + if (ok_shorter == r.ok_shorter) + res = (c==0)?longer:c; + else if (ok_shorter) + res = (c==0)?max(longer,0):c; + else // (r.ok_shorter) + res = (c==0)?min(longer,0):c; +// fprintf(stderr, "cmp(<%s,%s>, <%s,%s>) = %i\n", +// str.c_str(), ok_shorter?"true":"false", +// r.str.c_str(), r.ok_shorter?"true":"false", +// res); + return res; +} +bool server_manager::prefix::operator<(prefix const &r) const +{ + return cmp(r) < 0; +} +bool server_manager::prefix::operator==(prefix const &r) const +{ + return cmp(r) == 0; +} +bool server_manager::prefix::operator>(prefix const &r) const +{ + return cmp(r) > 0; +} + +void +server_manager::set_opts(map > const & o) +{ + opts = o; +} + +void +server_manager::add_replace_server(shared_ptr srv, serverdata const &dat) +{ + servers.insert(make_pair(srv, dat)); + by_name.insert(make_pair(dat.name, srv)); + for (set::iterator i = dat.hosts.begin(); + i != dat.hosts.end(); ++i) + by_host.insert(make_pair(*i, srv)); + for (set::iterator i = dat.patterns.begin(); + i != dat.patterns.end(); ++i) + by_pattern.insert(make_pair(*i, srv)); +} + +void +server_manager::delist_server(shared_ptr srv) +{ + serverdata sd; + map, serverdata>::iterator k = servers.find(srv); + if (k != servers.end()) + { + sd = k->second; + servers.erase(k); + } + map >::iterator j = by_name.find(sd.name); + if (j == by_name.end()) + return; + by_name.erase(j); + map >::iterator i; + for (set::iterator j = sd.hosts.begin(); + j != sd.hosts.end(); ++j) + { + i = by_host.find(*j); + if (i != by_host.end()) + by_host.erase(i); + } + for (set::iterator j = sd.patterns.begin(); + j != sd.patterns.end(); ++j) + { + i = by_pattern.find(*j); + if (i != by_pattern.end()) + by_pattern.erase(i); + } +} + +void +server_manager::reload_servers() +{ + if (!reader.begin_reading()) + { + return; + } + serverspec ss; + set all; + for (map >::iterator i = by_name.begin(); + i != by_name.end(); ++i) + all.insert(i->first); + while ((ss = reader.get_next()).valid) + { + shared_ptr srv; + set::iterator i = all.find(ss.name); + bool preexist(false); + if (i != all.end()) + { + all.erase(i); + srv = by_name.find(ss.name)->second; + preexist = true; + } + else + { + srv.reset(new server(*this)); + srv->me = srv; + } + srv->local = ss.local; + if (ss.local) + { + srv->arguments = ss.local_args; + if (ss.local_args.empty()) + throw errstr("'local' server has no arguments. Perhaps they need to be quoted?"); + } + else + { + srv->addr = ss.remote_host; + srv->port = ss.remote_port; + } + serverdata sd; + sd.name = ss.name; + sd.hosts = ss.hosts; + sd.patterns = ss.patterns; + if (preexist) + { + map, serverdata>::iterator i = servers.find(srv); + if (i->second == sd) + continue; + else + delist_server(srv); + } + add_replace_server(srv, sd); + } + reader.end_reading(); + for (set::iterator j = all.begin(); + j != all.end(); ++j) + { + map >::iterator i = by_name.find(*j); + if (i != by_name.end()) + delist_server(i->second); + } +} + +serversock +server_manager::connect_to_server(string const &host, + string const &pattern) +{ + if (!connections_allowed) + throw errstr("All servers are disabled."); + shared_ptr srv; + map >::iterator i; + if (!host.empty() && !by_host.empty()) + { + i = by_host.lower_bound(prefix(host, false)); + if (i != by_host.end() && i->first == prefix(host, false)) + srv = i->second; + } + if (!srv && !pattern.empty() && !by_pattern.empty()) + { + i = by_pattern.lower_bound(prefix(pattern, false)); + if (i != by_pattern.end() && i->first == prefix(pattern, false)) + srv = i->second; + } + if (srv) + { + sock s = srv->connect(); + serversock ss(s); + map, serverdata>::iterator i = servers.find(srv); + if (i == servers.end()) + throw errstr("server_manager is inconsistent"); + ss.srv = i->second.name; + ++total_connections; + if (srv->local && srv->connection_count == 1) + { + live.insert(srv); + } + return ss; + } + else { + throw errstr("Could not find a monotone instance for the pattern '" + pattern + "', perhaps make it less generic?."); + } +} +void +server_manager::disconnect_from_server(serversock const &s) +{ + map >::iterator i = by_name.find(s.srv); + if (i == by_name.end()) + return; + i->second->disconnect(); + if (i->second->local && i->second->pid == -1) + live.erase(i->second); + --total_connections; +} +string +server_manager::get_server_state(string const &name) +{ + map >::iterator i = by_name.find(name); + if (i != by_name.end()) + return boost::lexical_cast(i->second->get_state()); + throw errstr("No such server."); +} + +set +server_manager::list_servers(string const &state) +{ + set out; + for (map >::iterator i = by_name.begin(); + i != by_name.end(); ++i) + { + if (state.empty() || i->second->get_state() == state) + out.insert(i->first); + } + return out; +} + +void +server_manager::allow_connections(bool allow) +{ + connections_allowed = allow; +} + +string +server_manager::start_stop_server(string const &srv, bool start) +{ + map >::iterator i = by_name.find(srv); + if (i != by_name.end()) { + i->second->enabled = start; + if (!start) + i->second->maybekill(); + return boost::lexical_cast(i->second->get_state()); + } else + throw errstr("No such server."); +} + +void +server_manager::kill_old_servers() +{ + set >::iterator i; + for (i = live.begin(); i != live.end(); ++i) + (*i)->maybekill(); +} ============================================================ --- usher/server_manager.hh 5dfcfa92583c640562517653691199f91250969c +++ usher/server_manager.hh 5dfcfa92583c640562517653691199f91250969c @@ -0,0 +1,80 @@ +#ifndef __SERVER_MANAGER_HH_ +#define __SERVER_MANAGER_HH_ + +#include "sock.hh" +#include "server.hh" + +#include +using std::string; +#include +using std::set; +#include +using std::map; +#include +using std::vector; + +#include +using boost::shared_ptr; + +struct serverlist_reader; + +struct serversock : public sock +{ + string srv; + serversock(sock const &o); + operator bool() {return !srv.empty();} +}; + +class server_manager +{ + struct serverdata + { + string name; + set hosts, patterns; + bool operator==(serverdata const &o); + }; + struct prefix + { + string str; + bool ok_shorter; + prefix(string const &s); + prefix(string const &s, bool b); + ~prefix(); + operator string(); + int cmp(prefix const &r) const; + bool operator<(prefix const &r) const; + bool operator==(prefix const &r) const; + bool operator>(prefix const &r) const; + }; + map, std::greater > by_host, by_pattern; + map > by_name; + set > live; + map, serverdata> servers; + bool connections_allowed; + int total_connections; + serverlist_reader &reader; + map > opts; + + void add_replace_server(shared_ptr srv, serverdata const &dat); + void delist_server(shared_ptr srv); + + friend class server; + +public: + server_manager(serverlist_reader &r); + bool get_connections_allowed() {return connections_allowed;} + int get_total_connections() {return total_connections;} + map > get_opts() {return opts;} + + void set_opts(map > const & o); + void allow_connections(bool allow=true); + string start_stop_server(string const &srv, bool start); + void reload_servers(); + serversock connect_to_server(string const &host, string const &pattern); + void disconnect_from_server(serversock const &s); + string get_server_state(string const &name); + set list_servers(string const &state); + void kill_old_servers(); +}; + +#endif ============================================================ --- usher/serverlist_reader.cc 5cf9b3232eb61007105bc92eb73f9a04368ae035 +++ usher/serverlist_reader.cc 5cf9b3232eb61007105bc92eb73f9a04368ae035 @@ -0,0 +1,5 @@ +#include "serverlist_reader.hh" + +serverlist_reader::~serverlist_reader() +{ +} ============================================================ --- usher/serverlist_reader.hh 9613a97c615bfb147dd4910eedc8cb1d9b9150f7 +++ usher/serverlist_reader.hh 9613a97c615bfb147dd4910eedc8cb1d9b9150f7 @@ -0,0 +1,27 @@ +#ifndef __SERVERLIST_READER_HH_ +#define __SERVERLIST_READER_HH_ + +#include +#include +#include + +struct serverspec +{ + std::string name; + std::set hosts, patterns; + bool local; + bool valid; + std::vector local_args; + std::string remote_host; + int remote_port; +}; + +struct serverlist_reader +{ + virtual ~serverlist_reader(); + virtual serverspec get_next() = 0; + virtual bool begin_reading() = 0; + virtual void end_reading() = 0; +}; + +#endif ============================================================ --- usher/sock.cc c7d1253b571db327663956556a53969b06defbbb +++ usher/sock.cc c7d1253b571db327663956556a53969b06defbbb @@ -0,0 +1,168 @@ +#include "sock.hh" +#include "buffer.hh" +#include "err.hh" + +#include +#include +#include +#include +#include + +using std::set; +using std::string; +#include +using std::cerr; + +#include +using boost::lexical_cast; + +sock::operator int() +{ + if (!s) + return -1; + else + return s[0]; +} + +sock::sock(int ss) +{ + s = new int[2]; + s[0] = ss; + s[1] = 1; + all_socks.insert(s); +} + +sock::sock(sock const & ss) +{ + s = ss.s; + if (s) + s[1]++; +} + +void +sock::deref() +{ + if (s && !(--s[1])) { + try { + close(); + } catch(errstr & e) { + // if you want it to throw errors, call close manually + } + delete[] s; + all_socks.erase(all_socks.find(s)); + } + s = 0; +} + +sock::~sock() +{ + deref(); +} + +sock const & +sock::operator=(int ss) +{ + deref(); + s = new int[2]; + all_socks.insert(s); + s[0]=ss; + return *this; +} + +sock const & +sock::operator=(sock const & ss) +{ + deref(); + s = ss.s; + if (s) + ++s[1]; + return *this; +} + +void +sock::close() +{ + if (!s || s[0] == -1) + return; + shutdown(s[0], SHUT_RDWR); + while (::close(s[0]) < 0) { + if (errno == EIO) + throw errstr("close failed", errno); + if (errno != EINTR) + break; + } + s[0]=-1; +} + +void +sock::close_all_socks() +{ + for (set::iterator i = all_socks.begin(); i != all_socks.end(); ++i) { + while (::close((*i)[0]) < 0) + if (errno != EINTR) + break; + } +} + +bool +sock::read_to(buffer & buf) +{ + if (!s) + return false; + char *p; + int n; + buf.getwrite(p, n); + n = read(s[0], p, n); + if (n < 1) { + close(); + return false; + } else + buf.fixwrite(n); + return true; +} + +bool +sock::write_from(buffer & buf) +{ + if (!s) + return false; + char *p; + int n; + buf.getread(p, n); + n = write(s[0], p, n); + if (n < 1) { + close(); + return false; + } else + buf.fixread(n); + return true; +} +std::set sock::all_socks; + +sock +start(string const & addr, int port) +{ + sock s = tosserr(socket(AF_INET, SOCK_STREAM, 0), "socket()"); + int yes = 1; + tosserr(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + &yes, sizeof(yes)), "setsockopt"); + sockaddr_in a; + memset (&a, 0, sizeof (a)); + if (!inet_aton(addr.c_str(), (in_addr *) &a.sin_addr.s_addr)) + throw errstr("bad ip address format", 0); + a.sin_port = htons(port); + a.sin_family = AF_INET; + tosserr(bind(s, (sockaddr *) &a, sizeof(a)), "bind"); + cerr<<"bound to "<(addrport.substr(c+1)); + return start(addr, port); +} ============================================================ --- usher/sock.hh 4b136726ab92c01b4378e5bb1c1b56e2a4c11acf +++ usher/sock.hh 4b136726ab92c01b4378e5bb1c1b56e2a4c11acf @@ -0,0 +1,29 @@ +#ifndef __SOCK_HH_ +#define __SOCK_HH_ + +#include +#include + +struct buffer; + +struct sock +{ + int *s; + static std::set all_socks; + operator int(); + sock(int ss); + sock(sock const & ss); + void deref(); + ~sock(); + sock const & operator=(int ss); + sock const & operator=(sock const & ss); + void close(); + static void close_all_socks(); + bool read_to(buffer & buf); + bool write_from(buffer & buf); +}; + +sock start(std::string const & addr, int port); +sock start(std::string const & addrport); + +#endif ============================================================ --- usher/usher.cc 24cdda41ec47e215ab8ea0223ff9e9b26d3cfe41 +++ usher/usher.cc 24cdda41ec47e215ab8ea0223ff9e9b26d3cfe41 @@ -0,0 +1,218 @@ +// -*- mode: C++; c-file-style: "gnu"; indent-tabs-mode: nil -*- +// Timothy Brownawell +// GPL v2 + +#include "administrator.hh" +#include "err.hh" +#include "channel.hh" +#include "server_manager.hh" +#include "dir_serverlist_reader.hh" +#include "query_client.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using std::vector; +using std::max; +using std::string; +using std::list; +using std::set; +using std::map; +using boost::lexical_cast; +using boost::shared_ptr; +using std::cerr; +using std::pair; +using std::make_pair; + +// defaults moved to near the top of administrator.cc + + +string getline(std::istream & in) +{ + string out; + char buf[256]; + do { + in.getline(buf, 256); + int got = in.gcount()-1; + if (got > 0) + out.append(buf, got); + } while (in.fail() && !in.eof()); + return out; +} + + + +bool reload_pending(false); +string conffile; + + +void sched_reload(int sig) +{ + reload_pending = true; +} + +struct pidfile +{ + string filename; + void initialize(string const & file) + { + filename = file; + std::ofstream ofs(filename.c_str()); + ofs<\n"; + exit (1); + } + } + dir_serverlist_reader reader(conffile); + server_manager manager(reader); + administrator admin(manager, conffile); + + + struct sigaction sa, sa_old; + sa.sa_handler = &sched_reload; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + while(sigaction(SIGHUP, &sa, &sa_old) == -1 && errno == EINTR); + sa.sa_handler = SIG_IGN; + while(sigaction(SIGPIPE, &sa, &sa_old) == -1 && errno == EINTR); + sa.sa_handler = sig_end; + while(sigaction(SIGTERM, &sa, &sa_old) == -1 && errno == EINTR); + while(sigaction(SIGINT, &sa, &sa_old) == -1 && errno == EINTR); + + if (admin.serverport == -1) + { + std::cerr<<"Cannot open port.\n"; + exit(1); + } + + std::list channels; + + done = false; + while (!done) { + fd_set rd, wr, er; + FD_ZERO (&rd); + FD_ZERO (&wr); + FD_ZERO (&er); + FD_SET (admin.serverport, &rd); + int nfds = admin.serverport; + channel *newchan = 0; + + for (std::list::iterator i = channels.begin(); + i != channels.end(); ++i) + i->add_to_select(nfds, rd, wr, er); + + admin.add_to_select(nfds, rd, wr, er); + + timeval timeout; + timeout.tv_sec = 10; + timeout.tv_usec = 0; + int r = select(nfds+1, &rd, &wr, &er, &timeout); + + if (r < 0 && errno != EINTR) { + perror ("select()"); + exit (1); + } + if (done) + return 0; + if (FD_ISSET(admin.serverport, &rd)) { + try { + struct sockaddr_in client_address; + unsigned int l = sizeof(client_address); + memset(&client_address, 0, l); + sock cli = tosserr(accept(admin.serverport, (struct sockaddr *) + &client_address, &l), "accept()"); + if (manager.get_connections_allowed()) + newchan = new channel(cli, manager); + else { + char * dat; + int size; + make_packet(disabled, dat, size); + write(cli, dat, size); + delete[] dat; + } + } catch(errstr & s) { + cerr<<"During new connection: "<::iterator> finished; + for (std::list::iterator i = channels.begin(); + i != channels.end(); ++i) { + try { + i->process_selected(rd, wr, er); + if (i->is_finished()) + finished.push_back(i); + } catch (errstr & e) { + finished.push_back(i); + cerr<<"Error proccessing connection "<num<<": "<::iterator>::iterator i = finished.begin(); + i != finished.end(); ++i) + channels.erase(*i); + if (newchan) { + channels.push_back(*newchan); + delete newchan; + newchan = 0; + } + manager.kill_old_servers(); + if (reload_pending) + { + reload_pending = false; + admin.reload_conffile(); + } + + admin.process_selected(rd, wr, er); + } + return 0; +} ============================================================ --- usher.cc 8ab88aaf43ec1469fdd3cc95b9cfabb1d1b517e0 +++ usher.cc 8ab88aaf43ec1469fdd3cc95b9cfabb1d1b517e0 @@ -0,0 +1,1565 @@ +// -*- mode: C++; c-file-style: "gnu"; indent-tabs-mode: nil -*- +// Timothy Brownawell +// GPL v2 +// +// **************** SPECIALIZED FOR A SPECIFIC HOSTING SETUP ****************** +// **************************************************************************** +// +// USERPASS username password +// Required before any other command, so random people can't do bad things. +// If incorrect, the connection will be closed immediately. +// +// STATUS [servername] +// Get the status of a server, as named by the "server" lines in the +// config file. If a server is specified, the result will be one of: +// REMOTE - this is a remote server without active connections +// ACTIVE n - this server currently has n active connections +// WAITING - this (local) server is running, but has no connections +// SLEEPING - this (local) server is not running, but is available +// STOPPING n - this (local) server has been asked to stop, but still has +// n active connections. It will not accept further connections. +// STOPPED - this (local) server has been stopped, and will not accept +// connections. The server process is not running. +// SHUTTINGDOWN - the usher has been shut down, no servers are accepting +// connections. +// SHUTDOWN - the usher has been shut down, all connections have been closed, +// and all local server processes have been stopped. +// If no server is specified, the repsonse will be SHUTTINGDOWN, SHUTDOWN, +// WAITING, or ACTIVE (with n being the total number of open connections, +// across all servers). +// +// STOP servername +// Prevent the given local server from receiving further connections, and stop +// it once all connections are closed. The result will be the new status of +// that server: ACTIVE local servers become STOPPING, and WAITING and SLEEPING +// servers become STOPPED. Servers in other states are not affected. +// +// START servername +// Allow a stopped or stopping server to receive connections again. The result +// will be the new status of that server. (A server in the "STOPPING" state +// becomes ACTIVE, and a STOPPED server becomes SLEEPING. A server in some +// other state is not affected.) +// +// LIST [state] +// Returns a space-separated list of all servers. If a state is given, only +// list the servers that are in that state. +// +// SHUTDOWN +// Do not accept new connections for any servers, local or remote. Returns "ok". +// +// STARTUP +// Begin accepting connections again after a SHUTDOWN. Returns "ok". +// +// CONNECTIONS +// Returns the number of connections currently open. +// +// RELOAD +// Reload the config file (same as sending SIGHUP). The reply will be "ok", +// and will not be given until the config file has been reloaded. +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using std::vector; +using std::max; +using std::string; +using std::list; +using std::set; +using std::map; +using boost::lexical_cast; +using boost::shared_ptr; +using std::cerr; +using std::pair; +using std::make_pair; + +namespace { + +string hostname; +string project_dir; + +// defaults, overridden by command line +int listenport = 4691; +string listenaddr = "0.0.0.0"; +string monotone = "mtn"; +string logdir = "."; + +// keep local servers around for this many seconds after the last +// client disconnects from them (only accurate to ~10 seconds) +int const server_idle_timeout = 60; + +// ranges that dynamic (local) servers can be put on +int const minport = 15000; +int const maxport = 65000; +int const minaddr[] = {127, 0, 1, 1}; +int const maxaddr[] = {127, 254, 254, 254}; +int currport = 0; +int curraddr[] = {0, 0, 0, 0}; + +char const netsync_version = 6; + +string const greeting = " Hello! This is the monotone usher at localhost. What would you like?"; + +string const notfound = "!Sorry, I don't know where to find that."; + +string const disabled = "!Sorry, this usher is not currently accepting connections."; + +string const srvdisabled = "!Sorry, that server is currently disabled."; + +struct errstr +{ + std::string name; + int err; + errstr(std::string const & s): name(s), err(0) {} + errstr(std::string const & s, int e): name(s), err(e) {} +}; + +set get_project_names() +{ + set out; + DIR *projdir = opendir(project_dir.c_str()); + if (!projdir) { + std::cerr<<"Cannot scan project directory '"<d_name, ".") && strcmp(d->d_name, "..")) + out.insert(d->d_name); + } + closedir(projdir); + return out; +} + +int tosserr(int ret, std::string const & name) +{ + if (ret == -1) + throw errstr(name, errno); + if (ret < 0) + throw errstr(name, ret); + return ret; +} + +// packet format is: +// byte version +// byte cmd {100 if we send, 101 if we receive} +// uleb128 {size of everything after this} +// uleb128 {size of string} +// string +// { +// uleb128 {size of string} +// string +// } // only present if we're receiving + +// uleb128 is +// byte 0x80 | +// byte 0x80 | +// ... +// byte 0xff & +// +// the high bit says that this byte is not the last + +void make_packet(std::string const & msg, char * & pkt, int & size) +{ + size = msg.size(); + char const * txt = msg.c_str(); + char header[6]; + header[0] = netsync_version; + header[1] = 100; + int headersize; + if (size >= 128) { + header[2] = 0x80 | (0x7f & (char)(size+2)); + header[3] = (char)((size+2)>>7); + header[4] = 0x80 | (0x7f & (char)(size)); + header[5] = (char)(size>>7); + headersize = 6; + } else if (size >= 127) { + header[2] = 0x80 | (0x7f & (char)(size+1)); + header[3] = (char)((size+1)>>7); + header[4] = (char)(size); + headersize = 5; + } else { + header[2] = (char)(size+1); + header[3] = (char)(size); + headersize = 4; + } + pkt = new char[headersize + size]; + memcpy(pkt, header, headersize); + memcpy(pkt + headersize, txt, size); + size += headersize; +} + +struct buffer +{ + static int const buf_size = 16*1024; + static int const buf_reset_size = 8*1024; + char * ptr; + int readpos; + int writepos; + buffer(): readpos(0), writepos(0) + { + ptr = new char[buf_size]; + } + ~buffer(){delete[] ptr;} + buffer(buffer const & b) + { + ptr = new char[buf_size]; + memcpy(ptr, b.ptr, buf_size); + readpos = b.readpos; + writepos = b.writepos; + } + bool canread(){return writepos > readpos;} + bool canwrite(){return writepos < buf_size;} + void getread(char *& p, int & n) + { + p = ptr + readpos; + n = writepos - readpos; + } + void getwrite(char *& p, int & n) + { + p = ptr + writepos; + n = buf_size - writepos; + } + void fixread(int n) + { + if (n < 0) throw errstr("negative read\n", 0); + readpos += n; + if (readpos == writepos) { + readpos = writepos = 0; + } else if (readpos > buf_reset_size) { + memcpy(ptr, ptr+readpos, writepos-readpos); + writepos -= readpos; + readpos = 0; + } + } + void fixwrite(int n) + { + if (n < 0) + throw errstr("negative write\n", 0); + writepos += n; + } +}; + +struct sock +{ + int *s; + static set all_socks; + operator int() + { + if (!s) + return -1; + else + return s[0]; + } + sock(int ss) + { + s = new int[2]; + s[0] = ss; + s[1] = 1; + all_socks.insert(s); + } + sock(sock const & ss) + { + s = ss.s; + if (s) + s[1]++; + } + void deref() + { + if (s && !(--s[1])) { + try { + close(); + } catch(errstr & e) { + // if you want it to throw errors, call close manually + } + delete[] s; + all_socks.erase(all_socks.find(s)); + } + s = 0; + } + ~sock() + { + deref(); + } + sock const & operator=(int ss) + { + deref(); + s = new int[2]; + all_socks.insert(s); + s[0]=ss; + return *this; + } + sock const & operator=(sock const & ss) + { + deref(); + s = ss.s; + if (s) + ++s[1]; + return *this; + } + void close() + { + if (!s || s[0] == -1) + return; + shutdown(s[0], SHUT_RDWR); + while (::close(s[0]) < 0) { + if (errno == EIO) + throw errstr("close failed", errno); + if (errno != EINTR) + break; + } + s[0]=-1; + } + static void close_all_socks() + { + for (set::iterator i = all_socks.begin(); i != all_socks.end(); ++i) { + while (::close((*i)[0]) < 0) + if (errno != EINTR) + break; + } + } + bool read_to(buffer & buf) + { + if (!s) + return false; + char *p; + int n; + buf.getwrite(p, n); + n = read(s[0], p, n); + if (n < 1) { + close(); + return false; + } else + buf.fixwrite(n); + return true; + } + bool write_from(buffer & buf) + { + if (!s) + return false; + char *p; + int n; + buf.getread(p, n); + n = write(s[0], p, n); + if (n < 1) { + close(); + return false; + } else + buf.fixread(n); + return true; + } +}; +set sock::all_socks; + +bool check_address_empty(string const & addr, int port) +{ + sock s = tosserr(socket(AF_INET, SOCK_STREAM, 0), "socket()"); + int yes = 1; + tosserr(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + &yes, sizeof(yes)), "setsockopt"); + sockaddr_in a; + memset (&a, 0, sizeof (a)); + if (!inet_aton(addr.c_str(), (in_addr *) &a.sin_addr.s_addr)) + throw errstr("bad ip address format", 0); + a.sin_port = htons(port); + a.sin_family = AF_INET; + int r = bind(s, (sockaddr *) &a, sizeof(a)); + s.close(); + return r == 0; +} + +void find_addr(string & addr, int & port) +{ + if (currport == 0) { + currport = minport-1; + for(int i = 0; i < 4; ++i) + curraddr[i] = minaddr[i]; + } + do { + // get the next address in our list + if (++currport > maxport) { + currport = minport; + for (int i = 0; i < 4; ++i) { + if (++curraddr[i] <= maxaddr[i]) + break; + curraddr[i] = minaddr[i]; + } + } + port = currport; + addr = lexical_cast(curraddr[0]) + "." + + lexical_cast(curraddr[1]) + "." + + lexical_cast(curraddr[2]) + "." + + lexical_cast(curraddr[3]); + } while (!check_address_empty(addr, port)); +} + +sock start(string addr, int port) +{ + sock s = tosserr(socket(AF_INET, SOCK_STREAM, 0), "socket()"); + int yes = 1; + tosserr(setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + &yes, sizeof(yes)), "setsockopt"); + sockaddr_in a; + memset (&a, 0, sizeof (a)); + if (!inet_aton(addr.c_str(), (in_addr *) &a.sin_addr.s_addr)) + throw errstr("bad ip address format", 0); + a.sin_port = htons(port); + a.sin_family = AF_INET; + tosserr(bind(s, (sockaddr *) &a, sizeof(a)), "bind"); + cerr<<"bound to "<(addrport.substr(c+1)); + } catch (...) { + throw errstr("Could not parse address"); + } + return start(addr, port); +} + +sock make_outgoing(int port, string const & address) +{ + sock s = tosserr(socket(AF_INET, SOCK_STREAM, 0), "socket()"); + + struct sockaddr_in a; + memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; + a.sin_port = htons(port); + + if (!inet_aton(address.c_str(), (in_addr *) &a.sin_addr.s_addr)) + throw errstr("bad ip address format", 0); + + tosserr(connect(s, (sockaddr *) &a, sizeof (a)), "connect()"); + return s; +} + +int fork_server(const string & name, vector const & args) +{ +#ifdef RL_DEBUG + cerr << "Forking this command: "; + copy(args.begin(), args.end(), std::ostream_iterator(cerr, " ")); + cerr << std::endl; +#endif + + string logfile = logdir + "/" + name + ".log"; + int err[2]; + if ((err[1]=open(logfile.c_str(),O_CREAT|O_APPEND,0644)) < 0) + return false; + if ((err[0]=open(logfile.c_str(),O_RDONLY)) < 0) + { + close(err[1]); + return false; + } + int pid = fork(); + if (pid == -1) { + close(err[0]); + close(err[1]); + cerr<<"Failed to fork server.\n"; + return false; + } else if (pid == 0) { + close(err[0]); + close(0); + close(1); + close(2); + sock::close_all_socks(); + if (dup2(err[1], 1) < 0) { + exit(1); + } + close(err[1]); + if (dup2(1, 2) < 0) { + exit(1); + } + + char ** a = new char*[args.size()+1]; + for (unsigned int i = 0; i < args.size(); ++i) { + a[i] = new char[args[i].size()+1]; + memcpy(a[i], args[i].c_str(), args[i].size()+1); + } + a[args.size()] = 0; + + execvp(a[0], a); + perror("execvp failed\n"); + exit(1); + } else { + close(err[1]); + char head[256]; + int got = 0; + int r = 0; + bool line = false; + // the first line output on the server's stderr will be either + // "monotone: beginning service on : " or + // "monotone: network error: bind(2) error: Address already in use" + do { + r = read(err[0], head + got, 256 - got); + if (r) + cerr<<"Read '"< 0) { + for (int i = 0; i < r && !line; ++i) + if (head[got+i] == '\n') + line = true; + got += r; + } + } while(r >= 0 && !line && got < 256); + close(err[0]); + head[got] = 0; + if (string(head).find("beginning service") != string::npos) + return pid; + kill(pid, SIGKILL); + do {r = waitpid(pid, 0, 0);} while (r==-1 && errno == EINTR); + return -1; + } +} + +bool connections_allowed = true; +int total_connections = 0; + +struct serverstate +{ + enum ss {remote, active, waiting, sleeping, stopping, + stopped, shuttingdown, shutdown, unknown}; + ss state; + int num; + serverstate(): state(unknown), num(0) {} + serverstate const & operator=(string const & s) + { + if (s == "REMOTE") + state = remote; + else if (s == "ACTIVE") + state = active; + else if (s == "WAITING") + state = waiting; + else if (s == "SLEEPING") + state = sleeping; + else if (s == "STOPPING") + state = stopping; + else if (s == "STOPPED") + state = stopped; + else if (s == "SHUTTINGDOWN") + state = shuttingdown; + else if (s == "SHUTDOWN") + state = shutdown; + return *this; + } + bool operator==(string const & s) + { + serverstate foo; + foo = s; + return foo.state == state; + } +}; +std::ostream & operator<<(std::ostream & os, serverstate const & ss) +{ + switch (ss.state) { + case serverstate::remote: + os<<"REMOTE"; + break; + case serverstate::active: + os<<"ACTIVE "< >::iterator> by_host, by_pat; + map >::iterator by_name; + static map > servers_by_host; + static map > servers_by_pattern; + static map > servers_by_name; + static set > live_servers; + bool local; + int pid; + string arguments; + string addr; + int port; + int connection_count; + int last_conn_time; + server() : enabled(true), local(false), pid(-1), port(0), + connection_count(0), last_conn_time(0) + { + } + ~server() + { + yeskill(); + } + serverstate get_state() + { + serverstate ss; + ss.num = connection_count; + if (!connections_allowed) { + if (!total_connections) + ss.state = serverstate::shutdown; + else + ss.state = serverstate::shuttingdown; + } else if (connection_count) { + if (enabled) + ss.state = serverstate::active; + else + ss.state = serverstate::stopping; + } else if (!local) + ss.state = serverstate::remote; + else if (!enabled) + ss.state = serverstate::stopped; + else if (pid == -1) + ss.state = serverstate::sleeping; + else + ss.state = serverstate::waiting; + return ss; + } + void delist() + { + vector foo; + set_hosts(foo); + set_patterns(foo); + servers_by_name.erase(by_name); + by_name = 0; + } + void rename(string const & n) + { + shared_ptr me = by_name->second; + servers_by_name.erase(by_name); + by_name = servers_by_name.insert(make_pair(n, me)).first; + } + void set_hosts(vector const & h) + { + shared_ptr me = by_name->second; + map >::iterator c; + for (list >::iterator>::iterator + i = by_host.begin(); i != by_host.end(); ++i) + servers_by_host.erase(*i); + by_host.clear(); + for (vector::const_iterator i = h.begin(); i != h.end(); ++i) { + c = servers_by_host.find(*i); + if (c != servers_by_host.end()) { + cerr << "Removing duplicate for hostname " << *i << " in:" << std::endl + << " "; + list >::iterator>::iterator j; + bool first = true; + for (j = c->second->by_host.begin(); j != c->second->by_host.end(); ++j) + { + list >::iterator>::iterator j_saved + = j; + ++j; + cerr << (first ? "" : ", ") << (*j_saved)->second->by_name->first; + first = false; + if ((*j_saved)->first == *i) + { + servers_by_host.erase(*j_saved); + c->second->by_host.erase(j_saved); + } + } + cerr << std::endl + << " ... because it appeared in " << by_name->first << std::endl; + } + c = servers_by_host.insert(make_pair(*i, me)).first; + by_host.push_back(c); + } + } + void set_patterns(vector const & p) + { + shared_ptr me = by_name->second; + map >::iterator c; + for (list >::iterator>::iterator + i = by_pat.begin(); i != by_pat.end(); ++i) + servers_by_pattern.erase(*i); + by_pat.clear(); + for (vector::const_iterator i = p.begin(); i != p.end(); ++i) { + c = servers_by_pattern.find(*i); + if (c != servers_by_pattern.end()) { + cerr << "Removing duplicate for pattern " << *i << " in: " << std::endl + << " "; + list >::iterator>::iterator j; + bool first = true; + for (j = c->second->by_pat.begin(); j != c->second->by_pat.end(); ++j) + { + list >::iterator>::iterator j_saved + = j; + ++j; + cerr << (first ? "" : ", ") << (*j_saved)->second->by_name->first; + first = false; + if ((*j_saved)->first == *i) + { + servers_by_pattern.erase(*j_saved); + c->second->by_pat.erase(j_saved); + } + } + cerr << std::endl + << " ... because it appeared in " << by_name->first << std::endl; + } + c = servers_by_pattern.insert(make_pair(*i, me)).first; + by_pat.push_back(c); + } + } + sock connect() + { + if (!connections_allowed) + throw errstr("all servers disabled"); + if (!enabled) + throw errstr("server disabled"); + if (local && pid == -1) { + // server needs to be started + // we'll try 3 times, since there's a delay between our checking that + // a port's available and the server taking it + for (int i = 0; i < 3 && pid == -1; ++i) { + if (i > 0 || port == 0) + find_addr(addr, port); + vector args; + args.push_back(monotone); + args.push_back("serve"); + args.push_back("--ticker=dot"); + args.push_back("--bind=" + addr + ":" + lexical_cast(port)); + unsigned int n = 0, m = 0; + n = arguments.find_first_not_of(" \t"); + while (n != string::npos && m != string::npos) { + m = arguments.find_first_of(" ", n); + args.push_back(arguments.substr(n, m-n)); + n = arguments.find_first_not_of(" ", m); + } + pid = fork_server(by_name->first,args); + } + } + sock s = make_outgoing(port, addr); + if (local && !connection_count) { + live_servers.insert(by_name->second); + } + ++connection_count; + ++total_connections; + return s; + } + void disconnect() + { + --total_connections; + if (--connection_count || !local) + return; + last_conn_time = time(0); + maybekill(); + } + void maybekill() + { + if (!local) + return; + if (pid == -1) + return; + int difftime = time(0) - last_conn_time; + if (!connection_count + && (difftime > server_idle_timeout || !connections_allowed)) + yeskill(); + else if (waitpid(pid, 0, WNOHANG) > 0) { + pid = -1; + port = 0; + } + } + void yeskill() + { + if (local && pid != -1) { + kill(pid, SIGTERM); + int r; + do {r = waitpid(pid, 0, 0);} while (r==-1 && errno == EINTR); + pid = -1; + port = 0; + live_servers.erase(live_servers.find(by_name->second)); + } + } + string name() + { + if (local && port == 0) + return "dynamic local server"; + else + return addr + ":" + lexical_cast(port); + } +}; + +map > server::servers_by_host; +map > server::servers_by_pattern; +map > server::servers_by_name; +set > server::live_servers; + +string getline(std::istream & in) +{ + string out; + char buf[256]; + do { + in.getline(buf, 256); + int got = in.gcount()-1; + if (got > 0) + out.append(buf, got); + } while (in.fail() && !in.eof()); + return out; +} + +void make_server(string const & name) +{ + vector hosts, patterns; + string projdir = project_dir + "/" + name; + string desc = "-d " + projdir + "/database "; + desc += "--confdir " + projdir + " *"; + hosts.push_back(name + "." + hostname); + + // If it exists, find it. Otherwise, make a new one. + shared_ptr srv; + map >::iterator + i = server::servers_by_name.find(name); + if (i != server::servers_by_name.end()) { + srv = i->second; + i->second->delist(); + } else + srv = shared_ptr(new server); + + srv->by_name = server::servers_by_name.insert(make_pair(name, srv)).first; + srv->set_hosts(hosts); + srv->local = true; + srv->arguments = desc; +} + +shared_ptr get_server(string const & srv, string const & pat) +{ + map >::iterator i; + for (i = server::servers_by_host.begin(); + i != server::servers_by_host.end(); ++i) { + if (srv.find(i->first) == 0) { + return i->second; + } + } + for (i = server::servers_by_pattern.begin(); + i != server::servers_by_pattern.end(); ++i) { + if (pat.find(i->first) == 0) { + return i->second; + } + } + std::cerr<<"no server found for '"<(); +} + +shared_ptr get_server(string const & name) +{ + map >::iterator i; + for (i = server::servers_by_name.begin(); + i != server::servers_by_name.end(); ++i) { + if (name == i->first) { + return i->second; + } + } + return shared_ptr(); +} + +void kill_old_servers() +{ + set >::iterator i; + for (i = server::live_servers.begin(); i != server::live_servers.end(); ++i) { + (*i)->maybekill(); + } +} + +int extract_uleb128(char *p, int maxsize, int & out) +{ + out = 0; + int b = 0; + unsigned char got; + do { + if (b == maxsize) + return -1; + got = p[b]; + out += ((int)(p[b] & 0x7f))<<(b*7); + ++b; + } while ((unsigned int)(b*7) < sizeof(int)*8-1 && (got & 0x80)); + return b; +} + +int extract_vstr(char *p, int maxsize, string & out) +{ + int size; + out.clear(); + int chars = extract_uleb128(p, maxsize, size); + if (chars == -1 || chars + size > maxsize) { + return -1; + } + out.append(p+chars, size); + return chars+size; +} + +bool extract_reply(buffer & buf, string & host, string & pat) +{ + char *p; + int n, s; + buf.getread(p, n); + if (n < 4) return false; + p += 2; // first 2 bytes are header + n -= 2; + // extract size, and make sure we have the entire packet + int pos = extract_uleb128(p, n, s); + if (pos == -1 || n < s+pos) { + return false; + } + // extract host vstr + int num = extract_vstr(p+pos, n-pos, host); + if (num == -1) { + return false; + } + pos += num; + // extract pattern vstr + num = extract_vstr(p+pos, n-pos, pat); + if (num == -1) { + cerr<<"old-style reply.\n"; + pat = host; + host.clear(); + return true; + } + pos += num; + buf.fixread(pos+2); + return true; +} + +struct channel +{ + static int counter; + int num; + sock cli; + sock srv; + bool have_routed; + bool no_server; + buffer cbuf; + buffer sbuf; + shared_ptr who; + channel(sock & c): num(++counter), + cli(c), srv(-1), + have_routed(false), no_server(false) + { + char * dat; + int size; + make_packet(greeting, dat, size); + char *p; + int n; + sbuf.getwrite(p, n); + if (n < size) size = n; + memcpy(p, dat, size); + sbuf.fixwrite(size); + delete[] dat; + + cli.write_from(sbuf); + } + ~channel() + { + if (who && !no_server) + who->disconnect(); + } + bool is_finished() + { + return (cli == -1) && (srv == -1); + } + void add_to_select(int & maxfd, fd_set & rd, fd_set & wr, fd_set & er) + { + int c = cli; + int s = srv; + + if (c > 0) { + FD_SET(c, &er); + if (cbuf.canwrite()) + FD_SET(c, &rd); + if (sbuf.canread()) + FD_SET(c, &wr); + maxfd = max(maxfd, c); + } + if (s > 0) { + FD_SET(s, &er); + if (sbuf.canwrite()) + FD_SET(s, &rd); + if (cbuf.canread()) + FD_SET(s, &wr); + maxfd = max(maxfd, s); + } + } + bool process_selected(fd_set & rd, fd_set & wr, fd_set & er) + { + int c = cli; + int s = srv; +/* NB: read oob data before normal reads */ + if (c > 0 && FD_ISSET(c, &er)) { + char d; + errno = 0; + if (recv(c, &d, 1, MSG_OOB) < 1) + cli.close(), c = -1; + else + send(s, &d, 1, MSG_OOB); + } + if (s > 0 && FD_ISSET(s, &er)) { + char d; + errno = 0; + if (recv(s, &d, 1, MSG_OOB) < 1) + srv.close(), s = -1; + else + send(c, &d, 1, MSG_OOB); + } + + char *p=0; + int n; + + if (c > 0 && FD_ISSET(c, &rd)) { + if (!cli.read_to(cbuf)) c = -1; + if (!have_routed) { + string reply_srv, reply_pat; + if (extract_reply(cbuf, reply_srv, reply_pat)) { + who = get_server(reply_srv, reply_pat); + if (who && who->enabled) { + try { + srv = who->connect(); + have_routed = true; + s = srv; + } catch (errstr & e) { + cerr<<"cannot contact server "<name()<<"\n"; + no_server = true; + } + } else { + char * dat; + int size; + sbuf.getwrite(p, n); + if (who) + make_packet(srvdisabled, dat, size); + else + make_packet(notfound, dat, size); + if (n < size) size = n; + memcpy(p, dat, size); + sbuf.fixwrite(size); + delete[] dat; + no_server = true; + } + } + } + } + if (s > 0 && FD_ISSET(s, &rd)) { + if (!srv.read_to(sbuf)) s = -1; + } + + if (c > 0 && FD_ISSET(c, &wr)) { + if (!cli.write_from(sbuf)) c = -1; + } + if (s > 0 && FD_ISSET(s, &wr)) { + if (!srv.write_from(cbuf)) s = -1; + } + + // close sockets we have nothing more to send to + if (c < 0 && !cbuf.canread()) { + srv.close(), s = -1; + } + if ((no_server || have_routed && s < 0) && !sbuf.canread()) { + cli.close(), c = -1; + } + return true; + } +}; +int channel::counter = 0; + +bool reload_pending; +map admins; +string conffile; + +struct token +{ + enum type {key, str, hex, notok}; + type t; + string s; +}; + +token readtok(std::istream & is) +{ + char c; + do + is.get(c); + while(is && isspace(c)); + token tok; + if(!is) { + tok.t = token::notok; + return tok; + } + switch(c) { + case '[': + is.get(c); + tok.t = token::hex; + do { + tok.s += c; + is.get(c); + } while(is && c != ']'); + break; + case '"': + is.get(c); + tok.t = token::str; + do { + tok.s += c; + is.get(c); + if(c == '\\') { + is.get(c); + tok.s += c; + is.get(c); + } + } while(is && c != '"'); + break; + default: + tok.t = token::key; + do { + tok.s += c; + is.get(c); + } while(is && !isspace(c)); + } + return tok; +} + + +void sched_reload(int sig) +{ + reload_pending = true; +} + +void reload_conffile(string const & file); + +struct administrator +{ + sock port; + struct cstate + { + bool auth; + bool rdone; + string buf; + cstate(): auth(false), rdone(false) {} + }; + list > conns; + administrator(): port(-1) + {} + bool process(cstate & cs) + { + unsigned int n = cs.buf.find("\n"); + if (n == cs.buf.npos) + return true; + string l = cs.buf.substr(0, n); + cs.buf.erase(0, n+1); + std::istringstream iss(l); + string cmd; + iss>>cmd; + if (cmd == "USERPASS") { + string user, pass; + iss>>user>>pass; + map::iterator i = admins.find(user); + if (i == admins.end() || i->second != pass) { + cerr<<"Failed admin login.\n"; + return false; + } else { + if (cs.auth == true) + return false; + cs.auth = true; + return process(cs); + } + } else if (cmd == "STATUS") { + string srv; + iss>>srv; + std::ostringstream oss; + if (srv.empty()) { + serverstate ss; + ss.num = total_connections; + if (connections_allowed) { + if (total_connections) + ss.state = serverstate::active; + else + ss.state = serverstate::waiting; + } else { + if (total_connections) + ss.state = serverstate::shuttingdown; + else + ss.state = serverstate::shutdown; + } + oss< >::iterator i; + i = server::servers_by_name.find(srv); + if (i != server::servers_by_name.end()) + oss<second->get_state()<<"\n"; + else + oss<<"No such server.\n"; + } + cs.buf = oss.str(); + } else if (cmd == "ADD") { + string srv; + iss>>srv; + make_server(srv); + cs.buf = "ok"; + } else if (cmd == "START") { + string srv; + iss>>srv; + std::ostringstream oss; + map >::iterator i; + i = server::servers_by_name.find(srv); + if (i != server::servers_by_name.end()) { + i->second->enabled = true; + oss<second->get_state()<<"\n"; + } else + oss<<"No such server.\n"; + cs.buf = oss.str(); + } else if (cmd == "STOP") { + string srv; + iss>>srv; + std::ostringstream oss; + map >::iterator i; + i = server::servers_by_name.find(srv); + if (i != server::servers_by_name.end()) { + i->second->enabled = false; + i->second->maybekill(); + oss<second->get_state()<<"\n"; + } else + oss<<"No such server.\n"; + cs.buf = oss.str(); + } else if (cmd == "LIST") { + string state; + iss>>state; + map >::iterator i; + for (i = server::servers_by_name.begin(); + i != server::servers_by_name.end(); ++i) { + if (state.empty() || i->second->get_state() == state) + cs.buf += (cs.buf.empty()?"":" ") + i->first; + } + cs.buf += "\n"; + } else if (cmd == "SHUTDOWN") { + connections_allowed = false; + kill_old_servers(); + cs.buf = "ok\n"; + } else if (cmd == "CONNECTIONS") { + cs.buf = lexical_cast(total_connections) + "\n"; + } else if (cmd == "RELOAD") { + reload_conffile(conffile); + cs.buf = "ok\n"; + } else if (cmd == "STARTUP") { + connections_allowed = true; + cs.buf = "ok\n"; + } else { + return true; + } + cs.rdone = true; + return true; + } + void initialize(string const & ap) + { + try { + port = start(ap); + } catch (errstr & s) { + cerr<<"Could not initialize admin port: "< >::iterator i = conns.begin(); + i != conns.end(); ++i) { + int c = i->second; + if (!i->first.rdone) + FD_SET(c, &rd); + else + FD_SET(c, &wr); + maxfd = max(maxfd, int(c)); + } + } + void process_selected(fd_set & rd, fd_set & wr, fd_set & er) + { + if (int(port) == -1) + return; + if (FD_ISSET(port, &rd)) { + try { + struct sockaddr_in addr; + unsigned int l = sizeof(addr); + memset(&addr, 0, l); + sock nc = tosserr(accept(port, (struct sockaddr *) + &addr, &l), "accept()"); + conns.push_back(make_pair(cstate(), nc)); + } catch(errstr & s) { + cerr<<"During new admin connection: "< >::iterator> del; + for (list >::iterator i = conns.begin(); + i != conns.end(); ++i) { + int c = i->second; + if (c <= 0) { +// cerr<<"Bad socket.\n"; + del.push_back(i); + } else if (FD_ISSET(c, &rd)) { + char buf[120]; + int n; + n = read(c, buf, 120); + if (n < 1) { + // cerr<<"Read failed.\n"; + del.push_back(i); + } + i->first.buf.append(buf, n); + if (!process(i->first)) { +// cerr<<"Closing connection...\n"; +// i->second.close(); + del.push_back(i); + } + } + else if (FD_ISSET(c, &wr)) { + int n = write(c, i->first.buf.c_str(), i->first.buf.size()); + if (n < 1) { +// cerr<<"Write failed.\n"; + del.push_back(i); + } else { + i->first.buf.erase(0, n); + if (i->first.buf.empty() && i->first.rdone) { +// cerr<<"Done.\n"; + del.push_back(i); + } + } + } + } + for (list >::iterator>::iterator i = del.begin(); + i != del.end(); ++i) { + conns.erase(*i); + } + } +}; + +struct pidfile +{ + string filename; + void initialize(string const & file) + { + filename = file; + std::ofstream ofs(filename.c_str()); + ofs< names = get_project_names(); + for(set::const_iterator i = names.begin(); i != names.end(); ++i) { + make_server(*i); + cerr<<"Server: "<<(*i)<<"\n"; + } + + for (map >::iterator + i = server::servers_by_name.begin(); + i != server::servers_by_name.end(); ++i) { + if (names.find(i->first) == names.end()) + i->second->delist(); + } + cerr<<"Reload complete.\n"; +} + +};//namespace anonymous + +int main (int argc, char **argv) +{ + pidfile pf; +// administrator admin; + { + int i; + for (i = 1; i < argc; ++i) { + if (string(argv[i]) == "-p") + pf.initialize(argv[++i]); + else + conffile = argv[i]; + } + if (conffile.empty() || i != argc) { + cerr<<"Usage:\n"; + cerr<<"\tusher [-p pidfile] \n"; + exit (1); + } + } + reload_conffile(conffile); + + + struct sigaction sa, sa_old; + sa.sa_handler = &sched_reload; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + while(sigaction(SIGHUP, &sa, &sa_old) == -1 && errno == EINTR); + sa.sa_handler = SIG_IGN; + while(sigaction(SIGPIPE, &sa, &sa_old) == -1 && errno == EINTR); + sa.sa_handler = sig_end; + while(sigaction(SIGTERM, &sa, &sa_old) == -1 && errno == EINTR); + while(sigaction(SIGINT, &sa, &sa_old) == -1 && errno == EINTR); + + try { + h = start(listenaddr, listenport); + } catch (errstr & s) { + std::cerr<<"Error while opening socket: "< channels; + + done = false; + while (!done) { + fd_set rd, wr, er; + FD_ZERO (&rd); + FD_ZERO (&wr); + FD_ZERO (&er); + FD_SET (h, &rd); + int nfds = h; + channel *newchan = 0; + + for (std::list::iterator i = channels.begin(); + i != channels.end(); ++i) + i->add_to_select(nfds, rd, wr, er); + + admin.add_to_select(nfds, rd, wr, er); + + timeval timeout; + timeout.tv_sec = 10; + timeout.tv_usec = 0; + int r = select(nfds+1, &rd, &wr, &er, &timeout); + + if (r < 0 && errno != EINTR) { + perror ("select()"); + exit (1); + } + if (done) + return 0; + if (FD_ISSET(h, &rd)) { + try { + struct sockaddr_in client_address; + unsigned int l = sizeof(client_address); + memset(&client_address, 0, l); + sock cli = tosserr(accept(h, (struct sockaddr *) + &client_address, &l), "accept()"); + if (connections_allowed) + newchan = new channel(cli); + else { + char * dat; + int size; + make_packet(disabled, dat, size); + write(cli, dat, size); + delete[] dat; + } + } catch(errstr & s) { + cerr<<"During new connection: "<::iterator> finished; + for (std::list::iterator i = channels.begin(); + i != channels.end(); ++i) { + try { + i->process_selected(rd, wr, er); + if (i->is_finished()) + finished.push_back(i); + } catch (errstr & e) { + finished.push_back(i); + cerr<<"Error proccessing connection "<num<<": "<::iterator>::iterator i = finished.begin(); + i != finished.end(); ++i) + channels.erase(*i); + if (newchan) { + channels.push_back(*newchan); + delete newchan; + newchan = 0; + } + kill_old_servers(); + if (reload_pending) + reload_conffile(conffile); + + admin.process_selected(rd, wr, er); + } + return 0; +} ============================================================ --- webhost.conf 5bcbdd89e688a5104075fd4323560238070012ef +++ webhost.conf 5bcbdd89e688a5104075fd4323560238070012ef @@ -0,0 +1,5 @@ +# Configuration file for Apache +Alias /webhost WWWDIR + + AllowOverride all + ============================================================ --- webhost.cronjob 839760463ef4f93e3f346df0559805db3506842a +++ webhost.cronjob 839760463ef4f93e3f346df0559805db3506842a @@ -0,0 +1,20 @@ +#!/bin/sh +# remove old files from graphs/ until its less that the specified +# maximum size +# +cd GRAPHDIR +CONFFILE="CONFDIR/hostconfig" + +MAXNUM=$(grep '^graphdir_size' $CONFFILE \ + | sed 's/^[^"]*"\([^"]*\)"[^"]*$/\1/g') + +NUM=$(du -s graphs/ | awk '{print $1;}') + +for i in $(ls -utr graphs/); do + if ((NUM < MAXNUM)); then + break; + fi + S=$(du -s graphs/$i | awk '{print $1;}') + rm -f graphs/$i + NUM=$((NUM - S)) +done ============================================================ --- webhost.initscript bf5f34cb46dc189669206a1c54aa4b14c8611738 +++ webhost.initscript bf5f34cb46dc189669206a1c54aa4b14c8611738 @@ -0,0 +1,33 @@ +#! /bin/sh +set -e + +. /lib/lsb/init-functions + +OPTS="--quiet --pidfile /var/run/usher.BINNAME.pid --chuid=USHERUSER" +START="--start --exec=BINDIR/usher.BINNAME --background --make-pidfile" +CONFFILE='CONFDIR/hostconfig' +LC_ALL=C; export LC_ALL + +case "$1" in + start) + log_begin_msg "Starting webhost server..." + start-stop-daemon $START $OPTS -- $CONFFILE || log_end_msg 1 + log_end_msg 0 + ;; + stop) + log_begin_msg "Stopping webhost server..." + start-stop-daemon --stop --oknodo $OPTS || log_end_msg 1 + log_end_msg 0 + ;; + + restart) + log_begin_msg "Restarting webhost server..." + start-stop-daemon --stop --oknodo --retry 30 $OPTS + start-stop-daemon $START $OPTS -- $CONFFILE || log_end_msg 1 + log_end_msg 0 + ;; + + *) + log_success_msg "Usage: /etc/init.d/NAME {start|stop|restart}" + exit 1 +esac ============================================================ --- www/JSON.php e74c9792a17996ea4e2ab7c313e24238e8022650 +++ www/JSON.php e74c9792a17996ea4e2ab7c313e24238e8022650 @@ -0,0 +1,791 @@ + + * @author Matt Knapp + * @author Brett Stimmerman + * @copyright 2005 Michal Migurski + * @license http://www.opensource.org/licenses/bsd-license.php + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 + */ + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_SLICE', 1); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_STR', 2); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_ARR', 3); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_OBJ', 4); + +/** + * Marker constant for Services_JSON::decode(), used to flag stack state + */ +define('SERVICES_JSON_IN_CMT', 5); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** + * Behavior switch for Services_JSON::decode() + */ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** + * Converts to and from JSON format. + * + * Brief example of use: + * + * + * // create a new instance of Services_JSON + * $json = new Services_JSON(); + * + * // convert a complexe value to JSON notation, and send it to the browser + * $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); + * $output = $json->encode($value); + * + * print($output); + * // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] + * + * // accept incoming POST data, assumed to be in JSON notation + * $input = file_get_contents('php://input', 1000000); + * $value = $json->decode($input); + * + */ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $ut8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var) + { + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + + foreach($properties as $property) + if(Services_JSON::isError($property)) + return $property; + + return '{' . join(',', $properties) . '}'; + } + + // treat it like a regular array + $elements = array_map(array($this, 'encode'), $var); + + foreach($elements as $element) + if(Services_JSON::isError($element)) + return $element; + + return '[' . join(',', $elements) . ']'; + + case 'object': + $vars = get_object_vars($var); + + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + + foreach($properties as $property) + if(Services_JSON::isError($property)) + return $property; + + return '{' . join(',', $properties) . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode($value); + + if(Services_JSON::isError($encoded_value)) + return $encoded_value; + + return $this->encode(strval($name)) . ':' . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + (($chrs{$c - 1} != '\\') || + ($chrs{$c - 1} == '\\' && $chrs{$c - 2} == '\\'))) { + // found a quote, we're in a string, and it's not escaped + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('pear_error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> ============================================================ --- www/about.php c77f4adb39b1e74bd1e936be60e008e069d020ee +++ www/about.php c77f4adb39b1e74bd1e936be60e008e069d020ee @@ -0,0 +1,28 @@ + + + + + +About this site + + + + + + + + + + + + + +
+
+ +This site provides free public monotone hosting. All you need to add your own project is an account here, which only requires you to provide a username and password. If you have questions or need assistance, please contact the site administrator at . + +
+
+ + ============================================================ --- www/admin-description.js d8fab719409fc04467c21a9670d51ebd8b1ebbc1 +++ www/admin-description.js d8fab719409fc04467c21a9670d51ebd8b1ebbc1 @@ -0,0 +1,25 @@ +ctrl = '../../admin-description_backend.php'; + +chdesc = function () { + status("Changing project description..."); + var args = {'project':project}; + args.description = getElement("description").value; + args.longdescription = getElement("longdescription").value; + args.action = "chdesc"; + call_server(ctrl, args, "chdesc", function(data){clearstatus();}); +} + +addLoadEvent(function() { + status("Loading..."); + var x = {'project':project}; + x.action = "getdesc"; + call_server(ctrl, x, "getdesc", function (data) { + if (data.description) { + getElement("description").value = data.description; + } + if (data.longdescription) { + getElement("longdescription").value = data.longdescription; + } + clearstatus(); + }); +}); ============================================================ --- www/admin-description.php f4d40babaff2b48eb133853bd2eb938bf01ce084 +++ www/admin-description.php f4d40babaff2b48eb133853bd2eb938bf01ce084 @@ -0,0 +1,7 @@ +
+Short description:
+
+Long description:
+
+ +
============================================================ --- www/admin-description_backend.php 31ebbd661f9f479a0238ebec278fbb2636bd3774 +++ www/admin-description_backend.php 31ebbd661f9f479a0238ebec278fbb2636bd3774 @@ -0,0 +1,30 @@ +)~"; + return preg_replace($pat, "<", $string); +} + +if ($action == "getdesc") { + print $json->encode(array( + "description" => file_get_contents($projwww . "/description"), + "longdescription" => file_get_contents($projwww . "/longdescription") + )); +} else if ($action === "chdesc") { + if(allowed('description')) { + file_put_contents($projwww . "/description", sanitize($args->description, 'a( href="(?!javascript)[^"\\\\]*")?'), LOCK_EX); + $ok_for_longdesc = 'a( href="(?!javascript)[^"\\\\]*")?, p, br, li, ul, ol, i, b, blockquote, dl, dd, dt, em, strong, tt, ecode'; + file_put_contents($projwww . "/longdescription", sanitize($args->longdescription, $ok_for_longdesc), LOCK_EX); + print $json->encode(array("description" => file_get_contents($projwww . "/description"), + "longdescription" => file_get_contents($projwww . "/longdescription"))); + } +} +$db->Close(); +?> ============================================================ --- www/admin-files.js 1d84e9f90e267d99e4bddffe5f2e3185e31a0952 +++ www/admin-files.js 1d84e9f90e267d99e4bddffe5f2e3185e31a0952 @@ -0,0 +1,71 @@ +ctrl = '../../admin-files_backend.php'; + +do_rmfile = function (name) { + status("removing " + name + "..."); + var x = {'project':project,"action":"rmfile","file":name}; + call_server(ctrl, x, "rmfile", function (data) { + hideElement("trow-" + name); + clearstatus(); + }); +} + +do_chfilecomment = function (name) { + var c = getElement("filecomment-" + name).value; + status("changing description of " + name + "..."); + var x = {'project':project,"action":"chfiledesc","filedesc":c,"file":name}; + call_server(ctrl, x, "chfiledesc", function (data) { + clearstatus(); + }); +} + +add_file_to_present = function (name, desc) { + appendChildNodes("currentfiles-body", TR({}, + TD(null, A({'href': "files/" + name}, name)), + TD(null, INPUT({'type':'text','size':40,'value':desc,'id':'filecomment-' + name})), + TD(null, BUTTON({'type':'button','onclick':'do_rmfile("' + name + '");'}, "Remove")), + TD(null, BUTTON({'type':'button','onclick':'do_chfilecomment("' + name + '");'}, "Change description")) + )); +} + +empty_row = function () { + return TR({}, + TD(null, INPUT({"type": "file", "size": 40, "name": "upfiles[]"})), + TD(null, INPUT({"type": "text", "size": 40, "name": "upfile_comments[]"})) + ); +} +morefiles = function() { + appendChildNodes("filetablebody", empty_row()); +} +row_display = function (row) { + return TR(null, map(partial(TD, null), row)); +} +setup_upload = function() { + replaceChildNodes("filelist", TABLE({"id":"filetable"}, + THEAD(null, row_display(["File to upload", "File description"])), + TFOOT(null, row_display(["",""])), + TBODY({"id":"filetablebody"})) + ); + morefiles(); +} + + +do_upload = function() { + status("Uploading page..."); + getElement("upload_form").target = "upload_frame"; + getElement("upload_form").submit(); +} +uploaddone = function() { + getElement("upload_form").reset(); + clearstatus(); +} + +addLoadEvent(function() { +// status("Loading..."); +// var x = {'project':project}; +// x.action = "getmaint"; +// call_server(ctrl, x, "getmaint", function (data) { +// set_maint_display(data.maintainers); + setup_upload(); +// clearstatus(); +// }); +}); ============================================================ --- www/admin-files.php cf293356f2e2925a5212262f24965413fd10d1d2 +++ www/admin-files.php cf293356f2e2925a5212262f24965413fd10d1d2 @@ -0,0 +1,34 @@ +
+ +
+This project has already released the following files:
+ + + + + + + + + +
File nameFile description
\n"; +?> +
+
+ + +

+ + +
+ +
============================================================ --- www/admin-files_backend.php 8109ee3be651cbd4d693b930060403c1a7149a46 +++ www/admin-files_backend.php 8109ee3be651cbd4d693b930060403c1a7149a46 @@ -0,0 +1,51 @@ +File uploadFile uploaded"; +} elseif ($action == "rmfile") { + header('Content-type: text/x-json'); + unlink($projwww . "/files/" . basename($args->file)); + unlink($projwww . "/files-about/" . basename($args->file)); + print $json->encode(array("ok" => "ok")); +} elseif ($action == "chfiledesc") { + header('Content-type: text/x-json'); + unlink($projwww . "/files-about/" . basename($args->file)); + file_put_contents($projwww . "/files-about/" . basename($args->file), $args->filedesc); + print $json->encode(array("ok" => "ok")); +} +$db->Close(); +?> ============================================================ --- www/admin-permissions.js 444fcd22988b44ffc3df1f64ece814c0e734d889 +++ www/admin-permissions.js 444fcd22988b44ffc3df1f64ece814c0e734d889 @@ -0,0 +1,95 @@ +ctrl = '../../admin-permissions_backend.php'; + +row_display = function (row) { + return TR(null, map(partial(TD, null), row)); +} +make_cbox_properties = function (row, who) { + var out = {}; + out['type'] = "checkbox"; + out['name'] = "maint_" + who + "[]"; + if(row[who]) { + out['checked'] = true; + } + return out; +} +maint_row = function (row) { + return TR({"class":"maintainerrow"}, + TD(null, INPUT({"type": "text", "name": "maint_username[]", "value": row.username})), + TD(null, INPUT(make_cbox_properties(row, "give"))), + TD(null, INPUT(make_cbox_properties(row, "access"))), + TD(null, INPUT(make_cbox_properties(row, "server"))), + TD(null, INPUT(make_cbox_properties(row, "upload"))), + TD(null, INPUT(make_cbox_properties(row, "description")))/*, + TD(null, INPUT(make_cbox_properties(row, "homepage"))), + TD(null, INPUT(make_cbox_properties(row, "resources")))*/ + ); +} +empty_row = function () { + return TR({"class":"maintainerrow"}, + TD(null, INPUT({"type": "text", "name": "maint_username[]"})), + TD(null, INPUT({"type": "checkbox", "name": "maint_give[]"})), + TD(null, INPUT({"type": "checkbox", "name": "maint_access[]"})), + TD(null, INPUT({"type": "checkbox", "name": "maint_server[]"})), + TD(null, INPUT({"type": "checkbox", "name": "maint_upload[]"})), + TD(null, INPUT({"type": "checkbox", "name": "maint_description[]"}))/*, + TD(null, INPUT({"type": "checkbox", "name": "maint_homepage[]"})), + TD(null, INPUT({"type": "checkbox", "name": "maint_resources[]"}))*/ + ); +} +maint_addrow = function() { + appendChildNodes("maintainertablebody", empty_row()); +} +set_maint_display = function(data) { + var labels = []; + replaceChildNodes("maintainerlist", TABLE({"id":"maintainertable","class":"hilighttable"}, + THEAD(null, row_display(["Username", "Edit maintainers", "Control keys", "Control server", "Upload files", "Edit description"/*, "Edit homepage", "Change resources"*/])), + TFOOT(null, row_display(["","","","","",""/*,"",""*/])), + TBODY({"id":"maintainertablebody"}, map(maint_row, data))) + ); + maint_addrow(); +} +read_maint_line = function(row) { + var out = {}; + for(var i = 0; i < row.childNodes.length; ++i) { + var cur = row.childNodes[i].firstChild; + var nm = getNodeAttribute(cur,"name"); + nm = nm.replace(/.*_(.*)\[\]/, "$1"); + if (nm == "username") { + out[nm] = cur.value; + } else { + out[nm] = cur.checked; + } + } + return out; +} + + +chmaint = function () { + status("Changing maintainers..."); + var args = {'project':project}; + args.action = "chmaint"; + var lst = getElement("maintainertablebody").childNodes; + var maints = []; + for(var i = 0; i < lst.length; ++i) { + var line = read_maint_line(lst[i]); + if (line.username != "") { + maints.push(line); + } + } + args.newmaint = maints; + call_server(ctrl, args, "chmaint", function (data) { + set_maint_display(data.maintainers); + clearstatus(); + }); +} + + +addLoadEvent(function() { + status("Loading..."); + var x = {'project':project}; + x.action = "getmaint"; + call_server(ctrl, x, "getmaint", function (data) { + set_maint_display(data.maintainers); + clearstatus(); + }); +}); ============================================================ --- www/admin-permissions.php 1b1e6d4dc769ca39aa9dd01a5a8f7b3c4438c2bc +++ www/admin-permissions.php 1b1e6d4dc769ca39aa9dd01a5a8f7b3c4438c2bc @@ -0,0 +1,6 @@ +
+Project maintainers:
+

+ + +
============================================================ --- www/admin-permissions_backend.php fe7eeb8b0b6ad5f905dc314e51aee3e505852ed2 +++ www/admin-permissions_backend.php fe7eeb8b0b6ad5f905dc314e51aee3e505852ed2 @@ -0,0 +1,84 @@ +Execute($query, array($project)); + $out = array(); + if ($result) { + while (!$result->EOF) { + $perm['username'] = $result->fields[0]; + $perm['give'] = ($result->fields[1] == 1); + $perm['upload'] = ($result->fields[2] == 1); + $perm['homepage'] = ($result->fields[3] == 1); + $perm['access'] = ($result->fields[4] == 1); + $perm['server'] = ($result->fields[5] == 1); + $perm['description'] = ($result->fields[6] == 1); + $out[] = $perm; + $result->MoveNext(); + } + } + return $out; +} + +header('Content-type: text/x-json'); + +if ($action == "getmaint") { + print $json->encode(array("maintainers" => maintlist())); +} else if ($action == "chmaint") { + if(allowed('give')) { + $res = array(); + $ok = true; + $begun = false; + foreach ($args->newmaint as $i) { + if ($i->username === $username && !$i->give) { + $ok = false; + break; + } + } + do { + if(!$ok) { + $res["error"] = "You're not allowed to revoke your own permissions to edit maintainers."; + break; + } + $db->BeginTrans(); + $begun = true; + # pg_exec($db, "LOCK TABLE permissions"); + $result = $db->Execute("DELETE FROM permissions WHERE project=?", array($project)); + if (!$result) { + $res['error'] = 'Internal server error.'; + $ok = false; + break; + } + $fields = "username, project, give, upload, homepage, access, server, description"; + $query = sprintf("INSERT INTO permissions (%s) VALUES (?,?,?,?,?,?,?,?)", $fields); + $istmt = $db->Prepare($query); + foreach ($args->newmaint as $i) { + $result = $db->Execute($istmt, + array($i->username, + $project, + $i->give?1:0, + $i->upload?1:0, + $i->homepage?1:0, + $i->access?1:0, + $i->server?1:0, + $i->description?1:0)); + if (!$result) { + $res['error'] = 'Internal server error.'; + $ok = false; + break; + } + } + if($ok) + $res["maintainers"] = maintlist(); + } while(false); + if (!$ok && $begun) + $db->RollbackTrans(); + print $json->encode($res); + $db->CommitTrans(); + } +} +$db->Close(); +?> ============================================================ --- www/admin-resources.js de96b33ae298c6bba164631c769e420b658fbcda +++ www/admin-resources.js de96b33ae298c6bba164631c769e420b658fbcda @@ -0,0 +1,161 @@ +ctrl = '../../admin-resources_backend.php'; + +var make_item_ = {}; +var i = 0; + +close = function (which) { + var lst = getElement("rsclist").childNodes; + for(var i = 0; i < lst.length; ++i) { + var num = getNodeAttribute(lst[i],"name"); + if (num == "rsc_num_" + which) { + var closetxt = lst[i].firstChild.firstChild.nodeValue; + if (closetxt == "[Delete]") { + swapDOM(lst[i], P({}, "Deleted item")); + } else { + removeElement(lst[i]); + } + return; + } + } +} + +make_item_.link = function (row) { + var closer = "[Delete]"; + if (isNull(row.name)) { + closer = "[Cancel]"; + row.name = ""; + } + if (isNull(row.data)) { + row.data = ""; + } + ++i; + return DIV({"type":"link","class":"record","name":"rsc_num_"+i}, + SPAN({"class":"jslink","onclick":"close("+i+");","style":"float: right"}, closer), + "Type: link", BR(), + "Name: ", + INPUT({"type":"text","size":30,"name":"name","value":row.name}), BR(), + "URL: ", + INPUT({"type":"text","size":40,"name":"url","value":row.data}) + ); +} + +make_item_.irc = function (row) { + var data = {}; + if (!isNull(row.data)) { + data = evalJSON(row.data); + } + var closer = "[Delete]"; + if (isNull(row.name)) { + closer = "[Cancel]"; + row.name = ""; + } + if (isNull(data.host)) { + data.host = ""; + } + if (isNull(data.channel)) { + data.channel = ""; + } + if (isNull(data.logurl)) { + data.logurl = ""; + } + ++i; + return DIV({"type":"irc","class":"record","name":"rsc_num_"+i}, + SPAN({"class":"jslink","onclick":"close("+i+");","style":"float: right"}, closer), + "Type: IRC", BR(), + "Name: ", + INPUT({"type":"text","size":30,"name":"name","value":row.name}), BR(), + "Server: ", + INPUT({"type":"text","size":30,"name":"server","value":data.host}), BR(), + "Channel: ", + INPUT({"type":"text","size":30,"name":"channel","value":data.channel}), BR(), + "Log URL: ", + INPUT({"type":"text","size":40,"name":"logurl","value":data.logurl}) + ); +} + + +make_item = function (row) { + if (row.type == "unknown") { + return P({}, "Unknown item"); + } + try { + return make_item_[row.type](row); + } catch (e) { + return P({}, "Error: could not display item of type '" + row.type + "': " + e); + } +} + +addrow = function (type) { + appendChildNodes("rsclist", make_item_[type]({})); +} + +set_display = function(data) { + var labels = []; + replaceChildNodes("resourcediv", + DIV({"id":"rsclist"}, map(make_item, data)) + ); +} + +var read_line_ = {}; + +read_line_.link = function (row) { + var out = {"type":"link"}; + for(var i = 0; i < row.childNodes.length; ++i) { + var cur = row.childNodes[i]; + var nm = getNodeAttribute(cur,"name"); + if (nm == "name") {out.name = cur.value;} + if (nm == "url") {out.data = cur.value;} + } + return out; +} + +read_line_.irc = function (row) { + var out = {"type":"irc"}; + var data = {}; + for(var i = 0; i < row.childNodes.length; ++i) { + var cur = row.childNodes[i]; + var nm = getNodeAttribute(cur,"name"); + if (nm == "name") {out.name = cur.value;} + if (nm == "server") {data.host = cur.value;} + if (nm == "channel") {data.channel = cur.value;} + if (nm == "logurl") {data.logurl = cur.value;} + } + out.data = serializeJSON(data); + return out; +} + +read_line = function(row) { + var type = getNodeAttribute(row,"type"); + if (isNull(type) || type == "unknown") { return null; } + return read_line_[type](row); +} + +update = function () { + status("Updating..."); + var args = {'project':project}; + args.action = "chresources"; + var lst = getElement("rsclist").childNodes; + var resources = []; + for(var i = 0; i < lst.length; ++i) { + var line = read_line(lst[i]); + if (!isNull(line)) { + resources.push(line); + } + } + args.newresources = resources; + call_server(ctrl, args, "chresources", function (data) { + set_display(data.resources); + clearstatus(); + }); +} + + +addLoadEvent(function() { + status("Loading..."); + var x = {'project':project}; + x.action = "getresources"; + call_server(ctrl, x, "getresources", function (data) { + set_display(data.resources); + clearstatus(); + }); +}); ============================================================ --- www/admin-resources.php cc9f71016f146498bc68832f99a8bbb8c88fe426 +++ www/admin-resources.php cc9f71016f146498bc68832f99a8bbb8c88fe426 @@ -0,0 +1,7 @@ +
+Project resources: (no changes will be saved until you click update)
+

+ + + +
============================================================ --- www/admin-resources_backend.php c25e06b302ff66e55ecee79a85765f2e90339d2e +++ www/admin-resources_backend.php c25e06b302ff66e55ecee79a85765f2e90339d2e @@ -0,0 +1,52 @@ +encode(array("resources" => resourcelist())); +} else if ($action == "chresources") { + $res = array(); + $ok = true; + do { + $db->BeginTrans(); + # pg_exec($db, "LOCK TABLE resources"); + $result = $db->Execute("DELETE FROM resources WHERE project=?", array($project)); + if (!$result) { + $res['error'] = 'Internal server error. (1)'; + $ok = false; + break; + } + $istmt = $db->Prepare("INSERT INTO resources(project,name,type,data) VALUES(?,?,?,?)"); + foreach ($args->newresources as $i) { + if ($i->type == "link") { + $type = 0; + } elseif ($i->type == "irc") { + $type = 1; + } else { + $type = -1; + } + $values = array($project, + $i->name, + $type, + $i->data); + $result = $db->Execute($istmt, $values); + if (!$result) { + $res['error'] = 'Internal server error. (2)'; + $res['verboseError'] = sprintf("inserting into resources: %s, %s, %s, %s", $project, $i->name,$type,$i->data); + $ok = false; + break; + } + } + if($ok) + $res["resources"] = resourcelist(); + } while(false); + if (!$ok) + $db->RollbackTrans(); + else + $db->CommitTrans(); + print $json->encode($res); +} +$db->Close(); +?> ============================================================ --- www/admin-source_control.js a082da5d4a5e50cb274bb9328c9f9b1c2118043b +++ www/admin-source_control.js a082da5d4a5e50cb274bb9328c9f9b1c2118043b @@ -0,0 +1,82 @@ +ctrl = '../../admin-source_control_backend.php'; + +chwperm = function () { + status("Changing permissions..."); + var args = {'project':project}; + args.newperm = getElement("newperm").value; + args.action = "chwriters"; + call_server(ctrl, args, "chwperm", function(data){clearstatus();}); +} + +sendkey = function() { + status("Uploading key..."); + var args = {'project':project}; + args.action = "new_key"; + args.keydata = getElement("keydata").value; + call_server(ctrl, args, "sendkey", function(data){ + getElement("keydata").value = ""; + status("Key uploaded."); + }); +} + +resetdb = function() { + status("Resetting database..."); + var args = {'project':project}; + args.action = "resetdb"; + args.oath = getElement("oath").value; + call_server(ctrl, args, "resetdb", function(data){ + getElement("oath").value = ""; + status(data.ok); + }); +} + +chstate = function() { + status("Changing server state..."); + var args = {'project':project}; + if (getElement("chstate").value == "Disable server") { + args.action = "disable"; + } else { + args.action = "enable"; + } + call_server(ctrl, args, "chstate", function(data){ + set_state_display(data.status); + clearstatus(); + }); +} + +set_state_display = function(state){ + if (state == "") { + replaceChildNodes("serverstate", "(server not found)"); + } else { + replaceChildNodes("serverstate", state); + } + // set 'chstate' button to enable/disable appropriately + if (state.match("WAITING") || state == "RUNNING" || state == "SLEEPING") { + getElement("chstate").value = "Disable server"; + } else { + getElement("chstate").value = "Enable server"; + } +} + +restate = function() { + status("Refreshing server state..."); + var args = {'project':project}; + args.action = "refresh_state"; + call_server(ctrl, args, "restate", function(data){ + set_state_display(data.status); + clearstatus(); + }); +} + +addLoadEvent(function() { + status("Loading..."); + var x = {'project':project}; + x.action = "getsrv"; + call_server(ctrl, x, "getsrv", function (data) { + if (data.wperm) { + getElement("newperm").value = data.wperm; + } + set_state_display(data.status); + clearstatus(); + }); +}); ============================================================ --- www/admin-source_control.php 700d93d02ecec10bccee13823d193fcddd1b6ed3 +++ www/admin-source_control.php 700d93d02ecec10bccee13823d193fcddd1b6ed3 @@ -0,0 +1,49 @@ +
+ + + + +
+People allowed to write to the database:
+(key name)
+
+ +
+Keys already in the database:
+
+"); + } + } + } +?> +
+
+
+Upload a new pubkey.
+This is equivalent to someone with push access doing +
mtn read file-with-key-packet
+mtn --key-to-push=<keyname> push  ''
+Pubkey packet:
+
+ +
+Current server status is "server state"
+ + +
+This is to reset your database, for example if you had to do a rebuild. For this to work, you must type "I solemnly swear that I have a backup." in the box below.
+Note that Bad Things may happen if your server is running while you do this. ("Current server status" above should be SLEEPING or STOPPED or SHUTDOWN)
+ + +
============================================================ --- www/admin-source_control_backend.php 9379456da9dab9d28315a8be28a5f7a99e123ccf +++ www/admin-source_control_backend.php 9379456da9dab9d28315a8be28a5f7a99e123ccf @@ -0,0 +1,89 @@ +encode(array( + "status" => usherctrl("STATUS " . $project), + "wperm" => file_get_contents($projdir . "/write-permissions"), + )); +} else if ($action === "refresh_state") { + print $json->encode(array("status" => usherctrl("STATUS " . $project))); +} else if ($action === "disable" || $action === "enable") { + if(allowed('server')) { + if ($action === "disable") + $what = "STOP"; + else + $what = "START"; + print $json->encode(array("status" => usherctrl("$what $project"))); + } +} else if ($action === "resetdb") { + if(allowed('server')) { + if ($args->oath === "I solemnly swear that I have a backup.") { + $out = array(); + exec("rm $projdir/database 2>&1", $out, $res1); + exec("$monotone -d $projdir/database db init 2>&1", $out, $res2); + exec("chmod g+rw $projdir/database 2>&1", $out, $res3); + exec("cp $projdir/database $projdir/database.viewmtn"); + exec("cp $projdir/database $projdir/database.transfer"); + if ($res2 > 0 || $res3 > 0) { + $err = ""; + if ($res1) $err = $err . "Remove old database failed. "; + if ($res2) $err = $err . "Init new database failed. "; + if ($res3) $err = $err . "Set database permissions failed."; + print $json->encode(array("error" => $err, "verboseError" => $out)); + } else + print $json->encode(array("ok" => "Database reset... You *did* disable the server first, right?")); + } else { + print $json->encode(array("error" => "I'm sorry Dave, I can't let you do that.")); + } + } +} else if ($action === "new_key") { + if(allowed('access')) { + $args->keydata; + preg_match('/^\[[^ \]]* ([^\]]*)\]/', $args->keydata, $matches); + $keyname = $matches[1]; + if($keyname == null) + print $json->encode(array("error" => "Cannot extract key name.")); + else { + $out = array(); + file_put_contents($projdir . "/keyfile", $args->keydata, LOCK_EX); + exec("$monotone -d '$projdir/kdb' db init 2>&1", $out, $res1); + exec("$monotone -d '$projdir/kdb' read '$projdir/keyfile'", $out, $res2); + exec("$monotone --key-to-push '$keyname' -d '$projdir/kdb' push '$project.$hostname' '' -k '$hostkey' 2>&1", $out, $res3); + exec("rm '$projdir/kdb' '$projdir/keyfile'"); + if($res1 || $res2 || $res3) { + $outstr = ""; + foreach ($out as $i) + $outstr = $outstr . $i . "\n"; + print $json->encode(array("error" => "Key upload failed.", "verboseError" => $outstr)); + } else + print $json->encode(array("result" => "ok")); + } + } +} else if ($action === "chwriters") { + if(allowed('access')) { + file_put_contents($projdir . "/write-permissions", $args->newperm, LOCK_EX); + print $json->encode(array("result" => "ok")); + } +} +$db->Close(); +?> ============================================================ --- www/admin-web.js da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ www/admin-web.js da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- www/admin-web.php 011cc85d5bd32650ca04e0b1b40f2dfd40e1b19e +++ www/admin-web.php 011cc85d5bd32650ca04e0b1b40f2dfd40e1b19e @@ -0,0 +1,2 @@ +
+
============================================================ --- www/admin-web_backend.php da39a3ee5e6b4b0d3255bfef95601890afd80709 +++ www/admin-web_backend.php da39a3ee5e6b4b0d3255bfef95601890afd80709 ============================================================ --- www/admin.php fbd0d8196092c77c9c4fbd917f242cf2b113e79a +++ www/admin.php fbd0d8196092c77c9c4fbd917f242cf2b113e79a @@ -0,0 +1,57 @@ + + + + + + +Project admin page for <?=$project?> + + + + + + + + + + + + + +
+ + +
    + "Permissions", + 'source_control' => "Source Control", + 'files' => "Files", + 'description' => "Description", + /*'web' => "Web",*/ + 'resources' => "Resources"); + foreach ($pages as $k => $i) { + if ($k == $page) + print "
  • $i
  • "; + else + print "
  • $i
  • "; + } + ?> +
+ + +

You must login before being allowed to look at the maintainer pages

+ +
+ + ============================================================ --- www/administrator.js 6c90694cc02ec84ca391741656b00c9f4ef6692d +++ www/administrator.js 6c90694cc02ec84ca391741656b00c9f4ef6692d @@ -0,0 +1,55 @@ + +rmproj = function (name) { + status("Deleting project '"+name+"'..."); + if (!confirm("Are you sure you want to delete "+name+"?")) { + status("Delete canceled."); + return; + } + var args = {'project':name,'action':'delete_project'}; + call_server("proj-ctrl.php", args, "rmproj", function (data) { + removeElement("projdiv_"+name); + clearstatus(); + }); +} + +rmuser = function(name) { + status("Deleting user "+name+"..."); + if (!confirm("Are you sure you want to delete "+name+"?")) { + status("Delete canceled."); + return; + } + var args = {'who':name,'action':'delete_user'}; + call_server("user-ctrl.php", args, "rmuser", function (data) { + removeElement("userli_"+name); + clearstatus(); + }); +} + +op = function(name) { + status("Setting admin flag for "+name+"..."); + var args = {'who':name,'action':'op'}; + call_server("user-ctrl.php", args, "op", function (data) { + clearstatus(); + }); +} + +deop = function(name) { + status("Removing admin flag from "+name+"..."); + var args = {'who':name,'action':'deop'}; + call_server("user-ctrl.php", args, "deop", function (data) { + clearstatus(); + }); +} + +chuserpass = function(name) { + status("Resetting password for "+name+"..."); + var newpass = prompt('Enter a new password for ' + name, ''); + if (newpass == '') { + clearstatus(); + return; + } + var args = {'who':name,'new_password':newpass,'action':'chuserpass'}; + call_server("user-ctrl.php", args, "chuserpass", function (data) { + clearstatus(); + }); +} ============================================================ --- www/common-ctrl.php 5ea62c8285f5c48fad5e55441be84f6a8f484bec +++ www/common-ctrl.php 5ea62c8285f5c48fad5e55441be84f6a8f484bec @@ -0,0 +1,64 @@ +decode($GLOBALS['HTTP_RAW_POST_DATA']); + +$action = $args->action; if($_REQUEST['action']) $action = $_REQUEST['action']; +$project = $args->project; if($_REQUEST['project']) $project = $_REQUEST['project']; + +$projdir = $project_dir . "/" . basename($project); +$projwww = $www_dir . "/projects/" . basename($project); +$monotone = "$monotone --confdir '$projdir'"; + + +function getarg($name) { + global $args; + if ($_REQUEST[$name]) + return $_REQUEST[$name]; + else + return $args->$name; +} +function dirsafe($name) { + return ($name == basename($name)) && ($name != '..'); +} + +if (!$project) { + $project = basename(dirname($_SERVER['PHP_SELF'])); +} +function allowed($what) { + global $json, $permissions, $validuser, $username; + if(!$validuser) + print $json->encode(array("error" => "username or password is incorrect.")); + else if(!$permissions[$what]) + print $json->encode(array("error" => "You're not allowed to do that.")); + else + return true; + return false; +} + +$permissions = array( + "give" => false, // grant/revoke permissions for other users + "upload" => false, // upload files + "homepage" => false, // upload new home page + "access" => false, // upload key, write-permissions + "server" => false, // start/stop server, reset db + "description" => false + ); + +if ($validuser) { + $query = "SELECT give, upload, homepage, access, server, description"; + $query = $query . " FROM permissions WHERE username=? AND project=?"; + $result = $db->Execute($query, array($username, $project)); + if ($result) { + $permissions['username'] = $username; + $permissions['project'] = $project; + if ($result->RecordCount() == 1) { + $permissions['give'] = ($result->fields[0] == 1); + $permissions['upload'] = ($result->fields[1] == 1); + $permissions['homepage'] = ($result->fields[2] == 1); + $permissions['access'] = ($result->fields[3] == 1); + $permissions['server'] = ($result->fields[4] == 1); + $permissions['description'] = ($result->fields[5] == 1); + } + } +} +?> ============================================================ --- www/common-resource.php fbb14c31a5b356f9199190f90cee387a3c9a247d +++ www/common-resource.php fbb14c31a5b356f9199190f90cee387a3c9a247d @@ -0,0 +1,45 @@ +Execute($query, array($project)); + $out = array(); + if ($result) { + while (!$result->EOF) { + $r['name'] = $result->fields[0]; + if ($result->fields[1] == 0) { + $r['type'] = "link"; + } elseif ($result->fields[1] == 1) { + $r['type'] = "irc"; + } else { + $r['type'] = "unknown"; + } + $r['data'] = $result->fields[2]; + $out[] = $r; + $result->MoveNext(); + } + } + return $out; +} + +function get_linkstr($res) { + global $json; + $linkpat = "%s"; + if ($res['type'] == "link") { + return sprintf($linkpat, $res['data'], $res['name']); + } elseif ($res['type'] == "irc") { + $dat = $json->decode($res['data']); + $url = sprintf("irc://%s/%s", $dat->host, $dat->channel); + $ircline = sprintf($linkpat, $url, $res['name']); + if ($dat->logurl != "") { + $ircline .= " ("; + $ircline .= sprintf($linkpat, $dat->logurl, logs); + $ircline .= ")"; + } + return $ircline; + } elseif ($res['type'] == "unknown") { + return $res['name'] + ": " + $res['data']; + } +} +?> ============================================================ --- www/common.js 775a226f88575520d8f512e38400ca78a4f85168 +++ www/common.js 775a226f88575520d8f512e38400ca78a4f85168 @@ -0,0 +1,65 @@ +var hostname = "localhost"; +var userctrl = "user-ctrl.php"; + +var todo = {}; +var verberr = ""; +var respTxt = ""; + +// POST an object in JSON format, and get a return object also in JSON format +loadJSONPost = function (url, obj) { + respTxt = ""; + var data = serializeJSON(obj); + + var req = getXMLHttpRequest(); + req.open("POST", url, true); + req.setRequestHeader('Content-Type', 'text/x-json'); + var d = sendXMLHttpRequest(req, data); + d = d.addCallback(function(data){respTxt = data.responseText; return data;}); + d = d.addCallback(evalJSONRequest); + return d; +} + +call_server = function (url, args, nam, callback) { + var d = loadJSONPost(url, args); + todo[nam] = d; + d.addCallback(function (data) { + if (!isNull(data.error)) { + if(isNull(data.verboseError)) { + status("Error: " + data.error); + } else { + verberr = data.verboseError; + status("Error: " + data.error, BR(), SPAN({"class": "jslink", "onclick": "alert(verberr);"}, "(Click here for details)")); + } + } else { + callback(data); + } + }); + d.addErrback(function(err){ + status("Error: " + err, BR(), SPAN({"class": "jslink", "onclick": "alert(respTxt);"}, "(Click here for details)")); + }); +} + +userpass = function () { + var x = {}; + x.username = getElement("username").value; + x.password = getElement("password").value; + return x; +} + +userpassproj = function () { + var x = {}; + x.username = getElement("username").value; + x.password = getElement("password").value; + x.project = getElement("project").value; + return x; +} + + +clearstatus = function() { + replaceChildNodes('status'); +} + +status = function() { + replaceChildNodes("status", SPAN({"class": "jslink", "onclick": "clearstatus();", "style": "float: right;"}, "[X]"), arguments); +} + ============================================================ --- www/common.php b3d58f037ac01cd79b36aa512d985a4f5b53b281 +++ www/common.php b3d58f037ac01cd79b36aa512d985a4f5b53b281 @@ -0,0 +1,121 @@ +'; + +$confdir = 'CONFDIR'; +$conffile = $confdir . "/hostconfig"; + +# read conffile +$firstkey = '^[^\s"[]\S*'; +$key = '\s+[^\s"[]\S*'; +$str = '\s+"(?:\\\\"|\\\\\\\\|[^\\\\"])*"'; +$hex = '\s+\[[[:xdigit:]]*\]'; +$conf = file_get_contents($conffile); + +$pattern = "$firstkey|$key|$hex|$str"; +preg_match_all("/$pattern/", $conf, $splitconf, PREG_SET_ORDER); + +function nxt(&$arr) { + $foo = each($arr); + return preg_replace('/^\s*["[](.*)[\]"]\s*$/', '$1', $foo['value'][0]); +} +reset($splitconf); +while($v = each($splitconf)) { + $i = $v['value'][0]; + $i = preg_replace("/\n+\$/", '', $i); + $i = preg_replace("/^\n+/", '', $i); + $i = preg_replace('/^\s+/', '', $i); + $i = preg_replace('/\s+$/', '', $i); + if($i[0]=='"'||$i[0]=='[') + continue; + if($i == "userpass") { + $adminuser = nxt($splitconf); + $adminpass = nxt($splitconf); + } elseif($i == "hostname") { + $hostname = nxt($splitconf); + } elseif($i == "project_dir") { + $project_dir = nxt($splitconf); + } elseif($i == "adodb_path") { + $adodb_path = nxt($splitconf); + } elseif($i == "graph_dir") { + $graph_dir = nxt($splitconf); + } elseif($i == "www_dir") { + $www_dir = nxt($splitconf); + } elseif($i == "dbstring") { + $dbstring = nxt($splitconf); + } elseif($i == "hostkey") { + $hostkey = nxt($splitconf); + } elseif($i == "hostkeypass") { + $hostkeypass = nxt($splitconf); + } elseif($i == "adminaddr") { + # addr:port + list($adminaddr, $adminport) = split(":", nxt($splitconf)); + } elseif($i == "monotone") { + $monotone = nxt($splitconf); + } elseif($i == "site_owner_email") { + $site_owner_email = nxt($splitconf); + } elseif($i == "base_url") { + $base_url = nxt($splitconf); + } +} +reset($splitconf); + +include_once("JSON.php"); +$json = new Services_JSON(); + +function mktok($username, $shapass, $t) { + global $confdir; + $secfile = $confdir . "/secfile"; + $sstr = ""; + if (is_file($secfile)) { + $sstr = file_get_contents($secfile); + } + return sha1($username . $shapass . $t . $sstr); +} + +if ($_REQUEST['username'] && $_REQUEST['password']) { + $username = $_REQUEST['username']; + if ($_REQUEST['password'] != "") { + $shapass = sha1($_REQUEST['password']); + } +} else if ($_COOKIE['AUTH']) { + $auth = $json->decode(stripslashes($_COOKIE['AUTH'])); + do { + if ($auth->token != mktok($auth->username, $auth->password, + $auth->expiration)) { + break; + } + if ($auth->expiration < time()) { + break; + } + $username = $auth->username; + $shapass = $auth->password; + } while (false); +} else { + $username = ''; + $shapass = ''; +} + +include_once($adodb_path); +$validuser = false; +$administrator = false; +$pass_ok = false; +$db = &ADONewConnection( $dbstring ); +$result = $db->Execute("SELECT password, admin, email, active FROM users WHERE username=?", array($username)); +if ($result) { + $rows = $result->RecordCount(); + if ($rows == 1) { + if ($result->fields[0] == $shapass) { + $pass_ok = true; + if ($result->fields[3] == 1) { + $validuser = true; + } + $email = $result->fields[2]; + if ($result->fields[1] == 1) { + $administrator = true; + } + } + } +} +?> ============================================================ --- www/index.php 264a470d6d8750348eda8095abbd390b8dc0dc68 +++ www/index.php 264a470d6d8750348eda8095abbd390b8dc0dc68 @@ -0,0 +1,105 @@ + + + + + +Project List + + + + + + + + + + + + + +
+
+ +\n", $row[$pos_project]); + if ($administrator) { + printf('[Delete]', $row[$pos_project]); + } + printf("%s | \n", $row[$pos_project]); + printf("Project info page | \n", $row[$pos_project]); + if ($maintainer_p) { + printf("Maintainer Page | \n", $row[$pos_project]); + } + printf("mtn pull %s.%s \\*\n", $row[$pos_project], $hostname); + printf("
\n"); + if (is_file("projects/$row[$pos_project]/description")) { + print file_get_contents("projects/$row[$pos_project]/description"); + } + printf("
\n"); +} + +# First, display those maintained by the logged in user, if any +$projcnt = 0; +if ($validuser) { +# $query = "SELECT name, directory FROM projects WHERE name IN (SELECT project FROM permissions WHERE username = '%s')"; + $query= "SELECT name FROM projects WHERE name IN (SELECT project FROM permissions WHERE username=?)"; + $result = $db->Execute($query, array($username)); + if (!$result) { + printf("ERROR"); + } else { + $rows = $result->RecordCount(); + if ($rows > 0) { + printf("

Projects you help maintain:

"); + while (!$result->EOF) { + $row = $result->FetchRow(); + display_project_summary($row, 1); + $projcnt++; + } + } + } + +# $query = "SELECT name, directory FROM projects WHERE name NOT IN (SELECT project FROM permissions WHERE username = '%s')"; + $query = "SELECT name FROM projects WHERE name NOT IN (SELECT project FROM permissions WHERE username=?)"; + $result = $db->Execute($query, array($username)); + if (!$result) { + printf("ERROR"); + } else { + $rows = $result->RecordCount(); + if ($rows > 0) { + printf("

Projects you don't help maintain:

"); + while ($row = $result->FetchRow()) { + display_project_summary($row, 0); + $projcnt++; + } + } + } +} else { + $query = "SELECT name FROM projects"; + $result = $db->Execute($query); + if (!$result) { + printf("ERROR"); + } else { + $rows = $result->RecordCount(); + if ($rows > 0) { + printf("

Projects:

"); + while ($row = $result->FetchRow()) { + display_project_summary($row, 0); + $projcnt++; + } + } + } +} +if ($projcnt == 0) { + printf("No projects\n"); +} +$db->Close(); +?> + +
+
+ + ============================================================ --- www/login.php d22d0cba163de0db350017eb0427c7bdd7e0abe2 +++ www/login.php d22d0cba163de0db350017eb0427c7bdd7e0abe2 @@ -0,0 +1,182 @@ + $username, + 'password' => $shapass, + 'expiration' => $t, + 'token' => mktok($username, $shapass, $t)); + setcookie('AUTH', $json->encode($auth), 0, '/'); +} + +function page_head() { + global $validuser, $username, $location, $DOCTYPE; + $level = 'main'; + ?> + + + Login + + + + + + + + +
+ +
New user +
+

+Username:
+Password:
+Retype Password:
+Email:
+ +

+
+
+ +
Activate your account +
+

+Username:
+Password:
+Token:
+ +

+
+
+ + Logged out.
+ BeginTrans(); + $result = $db->Execute("SELECT * FROM users WHERE username=?", array($username)); + if (!$result) { + $res = "Internal server error."; + } else if ($result->RecordCount() == 0) { + $ires = $db->Execute("INSERT INTO users (username, password, admin, email, active) VALUES(?,?,0,?,0)", + array($username, $shapass, $_REQUEST["mail"])); + if (!$ires) { + $res = "Internal server error."; + $db->RollbackTrans(); + } else { + $t = mktok($username, $shapass, "validate"); + $mailbody = "Your username is " . $username . "\r\n" . + "Your password is " . $_REQUEST["password"] . "\r\n" . + "Your activation token is " . $t . "\r\n" . + "Please go to " . $base_url . "login.php?activate=activate and enter these values."; + $mailok = mail($_REQUEST["mail"], "Account at " . $hostname, $mailbody,"From: " . $site_owner_email); + if ($mailok) { + $res = "Added user $username."; + $validuser = true; + } else { + $db->RollbackTrans(); + $res = "Could not send activation email."; + } + } + } else { + $res = "That username is already taken."; + } + $db->CommitTrans(); + } + print ($res . "
\n"); + if ($validuser) { + activatebox(); + } else { + newuserbox(); + } +} elseif ($_REQUEST['activate']) { + if ($username == '') { + page_head(); + activatebox(); + } elseif ($pass_ok) { + $t = mktok($username, $shapass, "validate"); + if ($t == $_REQUEST["token"]) { + $res = $db->Execute("UPDATE users SET active=1 WHERE username=?", array($username)); + if ($res) { + docookie($username, $shapass); + $validuser = true; + page_head(); + print "Account activated."; + } else { + page_head(); + print "Internal server error."; + } + } else { + page_head(); + print "Token incorrect.
\n"; + activatebox(); + } + } else { + page_head(); + print "Username or password incorrect.
\n"; + activatebox(); + } +} else if ($_REQUEST['newpass']) { + if (!$validuser) { + $res = "Username or password incorrect."; + } else { + $newpass = $_REQUEST['newpass']; + if ($newpass == "") { + $res = "Your new password cannot be blank."; + } elseif ($newpass != $_REQUEST['newpass2']) { + $res = "You typed you password differently in each box."; + } else { + $query = "UPDATE users SET password=? WHERE username=?"; + $result = $db->Execute($query, array(sha1($newpass), $username)); + if(!result) { + $res = "Internal server error."; + } else { + $res = "Password changed."; + } + } + } + page_head(); + print $res; +} else { + if ($validuser) { + docookie($username, $shapass); + page_head(); + ?> + Logged in.
+ + Username or password incorrect.
+ +Back to where you were
+
============================================================ # www/logo.png is binary ============================================================ --- www/main.css ab84d1bfabe8c74ba8b62ab6835d0d478419abf1 +++ www/main.css ab84d1bfabe8c74ba8b62ab6835d0d478419abf1 @@ -0,0 +1,69 @@ + +body { + background-color: #fff; +} +#header { + background-color: #ddd; + height: 140px; + position: absolute; top: 5px; left: 5px; right: 5px; +} +#leftbar { + width: 225px; + position: absolute; left: 5px; top: 150px; +} +#leftbarmain { + width: auto; + border-width: 1px; border-style: solid; border-color: gold; + padding: 2px; background-color: #eee; +} +#leftbarproject { + width: auto; + border-width: 1px; border-style: solid; border-color: gold; + padding: 2px; background-color: #eee; +} +#projectname { + width: auto; + position: relative; margin: 5px; + border-width: 1px; border-style: solid; border-color: gold; + padding: 2px; background-color: #ddd; +} +#main { + position: absolute; left: 250px; top: 150px; + padding: 2px; +} +hr {width: 90%;} +#login {} +#status {background-color: #ccf;} +.record {background-color: #ccc; + border-width: 1px; border-color: #ff6; border-style: solid; + margin: 5px; padding: 5px;} +.jslink {cursor: pointer; color: blue; text-decoration: underline;} + +#page { + background-color: #ccc; + padding: .5em; +} + +.hilighttable tbody tr:hover { + background-color: #ddf; +} + +#pages { + background-color: #fff; + margin: 0px; +} +#pages li { + display: inline; + list-style: none; + padding: 0px 1em; + margin: 0px; + background-color: #999; +} +#pages li:hover { + background-color: #bbb; +} +#pages li a { +} +#pages li.selpage { + background-color: #ccc; +} ============================================================ --- www/proj-ctrl.php 2c4cb05995c4be6da7991a071143eb2ada8a327c +++ www/proj-ctrl.php 2c4cb05995c4be6da7991a071143eb2ada8a327c @@ -0,0 +1,188 @@ +File uploadedFile uploaded"; + $db->Close(); + exit; +} else if ($action === "upload_files") { + header('Content-type: text/html'); + print "File uploadFile uploaded"; + $db->Close(); + exit; +} + +function usherctrl($what) { + global $adminaddr, $adminport, $adminuser, $adminpass; + $addr = sprintf("tcp://%s:%s", $adminaddr, $adminport); + $fp = stream_socket_client($addr, $errno, $errstr, 30); + if (!$fp) { + return "Error: $errstr ($errno)"; + } else { + $out = ""; + fprintf($fp, "USERPASS %s %s\n%s\n", $adminuser, $adminpass, $what); + while (!feof($fp)) { + $out = $out . fgets($fp, 1024); + } + fclose($fp); + return $out; + } +} + +function maintlist() { + global $db, $project; + $fields = "username, give, upload, homepage, access, server, description"; + $query = sprintf("SELECT %s FROM permissions WHERE project=?", $fields); + $result = $db->Execute($query, array($project)); + $out = array(); + if ($result) { + while (!$result->EOF) { + $perm['username'] = $result->fields[0]; + $perm['give'] = ($result->fields[1] == 1); + $perm['upload'] = ($result->fields[2] == 1); + $perm['homepage'] = ($result->fields[3] == 1); + $perm['access'] = ($result->fields[4] == 1); + $perm['server'] = ($result->fields[5] == 1); + $perm['description'] = ($result->fields[6] == 1); + $out[] = $perm; + $result->MoveNext(); + } + } + return $out; +} + +header('Content-type: text/x-json'); + +if ($action == "new_project") { + if ($validuser) { + $db->BeginTrans(); + # pg_exec($db, "LOCK TABLE projects, permissions"); + $err = false; + + $result = $db->Execute("SELECT * FROM projects WHERE name=?", + array($project)); + do { + if(!preg_match('/^[a-zA-Z0-9-]*$/D', $project)) { + print $json->encode(array("error" => "Only letters, numbers, and dash are allowed in a project name.")); + $err = true; + break; + } + if(!$result) { + $err = true; + print $json->encode(array("error" => "Internal server error.")); + break; + } + if ($result->RecordCount()) { + print $json->encode(array("error" => "That project name is already taken.")); + $err = true; + break; + } + $projdir = $project_dir . '/'. $project; + $projwww = $www_dir . '/projects/'. $project; +# $query = "INSERT INTO projects (name, directory) VALUES ('%s', '%s')"; +# $result = pg_exec($db, sprintf($query, $safeproj, '/foobar')); + $result = $db->Execute("INSERT INTO projects (name) VALUES (?)", array($project)); + if(!$result) { + $err = true; + print $json->encode(array("error" => "Internal server error.")); + break; + } + $fields = "username, project, give, upload, homepage, access, server, description"; + $query = sprintf("INSERT INTO permissions (%s) VALUES (?,?,1,1,1,1,1,1)", $fields); + $result = $db->Execute($query, array($username, $project)); + if(!$result) { + $err = true; + print $json->encode(array("error" => "Internal server error.")); + break; + } + $out = array(); + mkdir($projdir); + symlink("$confdir/monotonerc", "$projdir/monotonerc"); + symlink("$confdir/keys", "$projdir/keys"); + mkdir($projwww); + mkdir("$projwww/files"); + mkdir("$projwww/files-about"); + symlink('../../admin.php', "$projwww/admin.php"); + symlink('../../project.php', "$projwww/index.php"); + symlink('../../viewmtn', "$projwww/viewmtn"); + exec("$monotone -d $projdir/database db init 2>&1", $out, $res2); + exec("cp $projdir/database $projdir/database.viewmtn"); + exec("cp $projdir/database $projdir/database.transfer"); + exec("chmod ug+rwX '$projdir' '$projdir'/database* 2>&1", $out, $res3); + if ($res1 || $res2 || $res3) { + print $json->encode(array("error" => "Internal server error", "verboseError" => $out)); + $err = true; + } + usherctrl("RELOAD"); + } while (false); + if ($err) + $db->RollbackTrans(); + else + print $json->encode(array("name" => $project)); + + $db->CommitTrans(); + } else + print $json->encode(array("error" => "username or password incorrect.")); +} else if ($action == "delete_project") { + if (!preg_match('/^[a-zA-Z0-9-]*$/D', $project)) { + print $json->encode(array("error" => "That is not a valid project name.")); + } else if (!$administrator) { + print $json->encode(array("error" => "You are not permitted to do that.")); + } else { + $db->Execute("DELETE FROM projects WHERE name=?", array($project)); + $db->Execute("DELETE FROM permissions WHERE project=?", array($project)); + $db->Execute("DELETE FROM resources WHERE project=?", array($project)); + $projdir = $project_dir . '/'. $project; + $projwww = $www_dir . '/projects/'. $project; + exec("rm -rf '$projdir' '$projwww'"); + print $json->encode(array("ok" => sprintf("'%s' deleted.", $project))); + } +} else + print $json->encode(array("error" => sprintf("'%s' not implemented.", $action))); +$db->Close(); +?> ============================================================ --- www/project.php 22f0453de251f599f486f4e43cdc4f660548ccc5 +++ www/project.php 22f0453de251f599f486f4e43cdc4f660548ccc5 @@ -0,0 +1,56 @@ +\n

" + . "This project has released the following files:

\n\n" + . "
    \n"; + $filediv = ""; + foreach($files as $i) { + if ($i == "." || $i == "..") + continue; + $inf = file_get_contents("files-about/$i"); + $filediv = $filediv + . "
  • $i: $inf
  • \n"; + } + $filediv_footer = "
\n"; + if ($filediv == "") { + return ""; + } + return $filediv_header . $filediv . $filediv_footer; +} + +?> + + +Project page for <?=$project?> + + + + + + + +
+ +"; + print file_get_contents('description'); + print ""; +}?> + +"; + print file_get_contents('longdescription'); + print "

"; +}?> + + + +
+ ============================================================ --- www/sidebar.js 591674ba9a1f37b50fe779982c88caaca5aa0145 +++ www/sidebar.js 591674ba9a1f37b50fe779982c88caaca5aa0145 @@ -0,0 +1,31 @@ +//needs common.js + +do_newuser = function () { + var x = userpass(); + x.action = "newuser"; + status("Adding user '" + x.username + "'..."); + call_server(userctrl, x, "newuser", function (data) { + status("Added user '" + data.username + "'."); + }); +} + +do_chpass = function () { + var x = userpass(); + x.action = "chpass"; + x.newpass = getElement("newpassword").value; + status("Changing password..."); + call_server(userctrl, x, "chpass", function (data) { + getElement("password").value = getElement("newpassword").value; + getElement("newpassword").value = ""; + clearstatus(); + }); +} + +do_newproj = function() { + status("Creating project..."); + var args = {'project':getElement('newproject').value}; + args.action = "new_project"; + call_server('proj-ctrl.php', args, "newproj", function(data) { + window.location = "projects/" + data.name + "/admin.php"; + }); +} ============================================================ --- www/sidebar.php bf888eca0fe896772daa4a00fe755d6ecfb1aa1b +++ www/sidebar.php bf888eca0fe896772daa4a00fe755d6ecfb1aa1b @@ -0,0 +1,73 @@ + +
+
+
+ +

+ You are .

+

+

+ + +

+
+ +
Returning user +
+

+ Username:
+
+ Password:
+
+ + +

+
+
+
New User +
+

+ +

+
+
+ +
+ + + +
+Project index
+User list
+About this site
+
+ +
+

+ + Project info
+ + Maintainer section
+ + Project resources:\n
    "; + print "
  • Source browser
  • "; + foreach (resourcelist() as $i) { + printf("
  • %s
  • \n", get_linkstr($i)); + } + print "
\n"; + print "
\n"; +} +?> +
============================================================ --- www/top.php 08f6997db49e7d8c65c79ea404da9d98b9c900c3 +++ www/top.php 08f6997db49e7d8c65c79ea404da9d98b9c900c3 @@ -0,0 +1,4 @@ + ============================================================ --- www/user-actions.php aa3667da94ee7a18543bbb487d3e658704602c20 +++ www/user-actions.php aa3667da94ee7a18543bbb487d3e658704602c20 @@ -0,0 +1,14 @@ +BeginTrans(); + $res = $db->Execute("UPDATE users SET email = ? WHERE username = ?", array($newmail, $username)); + if ($res) { + $db->CommitTrans(); + $email = $newmail; + } else { + $db->RollbackTrans(); + } +} +} +?> ============================================================ --- www/user-ctrl.php 9f01d97b2c58c2a1b8c547782d105f3edf093260 +++ www/user-ctrl.php 9f01d97b2c58c2a1b8c547782d105f3edf093260 @@ -0,0 +1,49 @@ +who; + +if ($action == 'delete_user') { + if (! $administrator) { + print $json->encode(array('error' => 'You are not permitted to do that.')); + exit; + } + $db->BeginTrans(); + $db->Execute("DELETE FROM users WHERE username=?", + array($who)); + $db->Execute("DELETE FROM permissions WHERE username=?", + array($who)); + $db->CommitTrans(); + print $json->encode(array('ok' => 'ok')); +} else if ($action == 'chuserpass') { + if (! $administrator) { + print $json->encode(array('error' => 'You are not permitted to do that.')); + exit; + } + $db->BeginTrans(); + $db->Execute("UPDATE USERS SET password=? WHERE username=?", + array(sha1($args->new_password), $who)); + $db->CommitTrans(); + print $json->encode(array('ok' => 'ok')); +} else if ($action == 'op') { + if (! $administrator) { + print $json->encode(array('error' => 'You are not permitted to do that.')); + exit; + } + $db->BeginTrans(); + $db->Execute("UPDATE USERS SET admin=1 WHERE username=?", + array($who)); + $db->CommitTrans(); + print $json->encode(array('ok' => 'ok')); +} else if ($action == 'deop') { + if (! $administrator) { + print $json->encode(array('error' => 'You are not permitted to do that.')); + exit; + } + $db->BeginTrans(); + $db->Execute("UPDATE USERS SET admin=0 WHERE username=?", + array($who)); + $db->CommitTrans(); + print $json->encode(array('ok' => 'ok')); +} else + print $json->encode(array('error' => sprintf("I don't know how to '%s'.", strip_tags($action)))); +?> ============================================================ --- www/user.php 877d668d0f7964e6929ddfab3f07d48483e39475 +++ www/user.php 877d668d0f7964e6929ddfab3f07d48483e39475 @@ -0,0 +1,46 @@ + + + + + + + +User page for <?=$username?> + + + + + + + + + + + +
+ +
Change password +
+New password:
+Retype password:
+ +
+
+ +
Email +
+Your current email address is .
+New email:
+ +
+
+ +
+ + ============================================================ --- www/userlist.php 003c0da953c921c559edb0250e8cf9196ecea768 +++ www/userlist.php 003c0da953c921c559edb0250e8cf9196ecea768 @@ -0,0 +1,66 @@ + + + + + +Project List + + + + + + + + + + + + + +
+
+ +%s\n", + $what, $who, $text); +} +function display_user($username, $admin) { + global $administrator; + printf("
  • \n", $username); + printf("%s\n", $username, $username); + if ($admin) { + print "| Site Administrator\n"; + } + if ($administrator) { + adminlink('chuserpass', $username, '[Reset password]'); + if ($admin) + adminlink('deop', $username, '[de-op]'); + else + adminlink('op', $username, '[op]'); + adminlink('rmuser', $username, '[Delete]'); + } + printf("
  • \n"); +} + +$query= "SELECT username, admin FROM users ORDER BY admin DESC"; +$result = $db->Execute($query); +if ($result) { + print "
      "; + $rows = $result->RecordCount(); + if ($rows > 0) { + while (!$result->EOF) { + $row = $result->FetchRow(); + display_user($row[0], $row[1]); + } + } + print "
    \n"; +} else + print "ERROR<\n"; +$db->Close(); +?> + +
    +
    + + ============================================================ --- www/viewmtn/config.py.example 9248282147264a3a9d09df9de7337b672e2d41f2 +++ www/viewmtn/config.py.example 97fbc51fb12393018862a3073aa718a25bfb6eac @@ -20,7 +20,11 @@ import sys # import sys +import os +import re +confdir = 'CONFDIR' +conffile = confdir + '/hostconfig' # default addresses should work without modification # if running viewmtn standalone. You must change these # if you run viewmtn with a web server, see INSTALL. @@ -31,15 +35,42 @@ running_under_apache2 = False # don't set it otherwise, it breaks any other configuration # including running standalone. running_under_apache2 = False +def file_tokens(fn): + f = open(fn, 'r') + data = f.read() + f.close() + tok_str = r'"(?:[^"]|\\")*"' + tok_key = r'[^"\s[]\S*' + tok_hex = r'\[[[:xdigit:]]*\]' + anytok = '(%s|%s|%s)' % (tok_key, tok_str, tok_hex) + return re.split(anytok, data) +config_data = {} + +for s in file_tokens(conffile): + if not re.match(r'^\s', s): + if re.match(r'^["[]', s): + config_data[lastkey].append(s[1:-1]) + else: + lastkey = s + config_data[lastkey] = [] + +def dynamic_uri_path(uri): + d = os.path.dirname(os.path.dirname(uri)) + return config_data['base_url'][0] + ('/projects/%s/viewmtn/' % os.path.basename(d)) +def static_uri_path(uri): + return config_data['base_url'][0] + '/viewmtn/static/' + # the path to the 'mtn' binary -monotone = '/usr/bin/mtn' +monotone = config_data['monotone'][0] # the monotone database to be shared out # everything in this database should be considered subject # to disclosure. So don't store your private key in # it! -dbfile = '/path/to/my/viewmtn.db' +def dbfile(uri): + d = os.path.dirname(os.path.dirname(uri)) + return (config_data['project_dir'][0] + '/%s/database.viewmtn') % os.path.basename(d) # highlight from http://andre-simon.de/ # if you don't have this available, just comment @@ -51,7 +82,7 @@ graphopts = { # in which viewmtn can output graph files # (you should set up a cronjob to delete old ones # periodically) - 'directory' : '/var/tmp/viewmtn-graph/', + 'directory' : config_data['graph_dir'][0], # a URL, relative or absolute, at which the files # in the 'graphdir' directory can be found. Should