From d3372da97568d5e1f35fa19787c8ec8af93a0435 Mon Sep 17 00:00:00 2001 From: fredrik Date: Fri, 14 Oct 2005 23:35:54 +0000 Subject: [PATCH] Transfer from CVS at SourceForge git-svn-id: svn+ssh://svn.dolda2000.com/srv/svn/repos/src/doldaconnect@356 959494ce-11ee-0310-bf91-de5d638817bd --- AUTHORS | 2 + COPYING | 340 ++ CVS/Entries | 16 + CVS/Repository | 1 + CVS/Root | 1 + ChangeLog | 62 + INSTALL | 182 ++ Makefile.am | 5 + NEWS | 0 README | 0 autopackage/CVS/Entries | 2 + autopackage/CVS/Repository | 1 + autopackage/CVS/Root | 1 + autopackage/dolcon.apspec.in | 48 + bootstrap | 3 + clients/CVS/Entries | 5 + clients/CVS/Repository | 1 + clients/CVS/Root | 1 + clients/Makefile.am | 13 + clients/gnome-trans-applet/CVS/Entries | 9 + clients/gnome-trans-applet/CVS/Repository | 1 + clients/gnome-trans-applet/CVS/Root | 1 + .../Dolcon_Transferapplet_Factory.server.in | 21 + clients/gnome-trans-applet/Makefile.am | 22 + clients/gnome-trans-applet/conduit-dclib.c | 314 ++ clients/gnome-trans-applet/conduit-pipe.c | 190 ++ clients/gnome-trans-applet/conduit.c | 145 + clients/gnome-trans-applet/conduit.h | 55 + clients/gnome-trans-applet/dctrmon | 93 + clients/gnome-trans-applet/dolcon-trans-applet.c | 229 ++ clients/gtk2/CVS/Entries | 10 + clients/gtk2/CVS/Repository | 1 + clients/gtk2/CVS/Root | 1 + clients/gtk2/Makefile.am | 21 + clients/gtk2/emacs-local | 13 + clients/gtk2/inpdialog.desc | 8 + clients/gtk2/main.c | 1815 +++++++++++ clients/gtk2/mainwnd.desc | 176 ++ clients/gtk2/makegdesc | 422 +++ clients/gtk2/pref.desc | 10 + clients/gtk2/progressbar.c | 344 ++ clients/gtk2/progressbar.h | 44 + clients/hellodolda.jpg | Bin 0 -> 264133 bytes clients/test.c | 51 + config/CVS/Entries | 7 + config/CVS/Repository | 1 + config/CVS/Root | 1 + config/Makefile.am | 21 + config/dc-filter | 99 + config/dc-filtercmd | 42 + config/doldacond.conf | 56 + config/locktouch.c | 27 + config/speedrec.c | 134 + configure.in | 213 ++ daemon/CVS/Entries | 31 + daemon/CVS/Repository | 1 + daemon/CVS/Root | 1 + daemon/Makefile.am | 30 + daemon/auth-krb5.c | 590 ++++ daemon/auth-pam.c | 332 ++ daemon/auth.c | 183 ++ daemon/auth.h | 70 + daemon/client.c | 998 ++++++ daemon/client.h | 85 + daemon/conf.c | 392 +++ daemon/conf.h | 75 + daemon/emacs-local | 13 + daemon/filenet.c | 473 +++ daemon/filenet.h | 138 + daemon/fnet-dc.c | 3302 ++++++++++++++++++++ daemon/log.c | 75 + daemon/log.h | 36 + daemon/main.c | 558 ++++ daemon/module.h | 65 + daemon/net.c | 1124 +++++++ daemon/net.h | 113 + daemon/search.c | 1183 +++++++ daemon/search.h | 132 + daemon/sysevents.h | 52 + daemon/tiger.c | 748 +++++ daemon/tiger.h | 28 + daemon/transfer.c | 738 +++++ daemon/transfer.h | 129 + daemon/ui.c | 2105 +++++++++++++ daemon/uiretref | 32 + daemon/utils.c | 737 +++++ daemon/utils.h | 193 ++ include/CVS/Entries | 2 + include/CVS/Repository | 1 + include/CVS/Root | 1 + include/Makefile.am | 2 + include/doldaconnect/CVS/Entries | 4 + include/doldaconnect/CVS/Repository | 1 + include/doldaconnect/CVS/Root | 1 + include/doldaconnect/uilib.h | 56 + include/doldaconnect/uimisc.h | 76 + include/doldaconnect/utils.h | 40 + lib/CVS/Entries | 9 + lib/CVS/Repository | 1 + lib/CVS/Root | 1 + lib/Makefile.am | 16 + lib/guile/CVS/Entries | 5 + lib/guile/CVS/Repository | 1 + lib/guile/CVS/Root | 1 + lib/guile/Makefile.am | 9 + lib/guile/autodl | 547 ++++ lib/guile/chatlog | 149 + lib/guile/dolcon-guile.c | 339 ++ lib/guile/dolcon/CVS/Entries | 4 + lib/guile/dolcon/CVS/Repository | 1 + lib/guile/dolcon/CVS/Root | 1 + lib/guile/dolcon/Makefile.am | 4 + lib/guile/dolcon/ui.scm | 74 + lib/guile/dolcon/util.scm | 40 + lib/initcmds.h | 138 + lib/makecmds | 49 + lib/uicmds | 130 + lib/uilib.c | 1108 +++++++ lib/uilib.h | 23 + lib/uimisc.c | 1055 +++++++ lib/utils.c | 239 ++ po/CVS/Entries | 6 + po/CVS/Repository | 1 + po/CVS/Root | 1 + po/ChangeLog | 11 + po/LINGUAS | 2 + po/Makevars | 41 + po/POTFILES.in | 7 + po/sv.po | 439 +++ 129 files changed, 24679 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 CVS/Entries create mode 100644 CVS/Repository create mode 100644 CVS/Root create mode 100644 ChangeLog create mode 100644 INSTALL create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 autopackage/CVS/Entries create mode 100644 autopackage/CVS/Repository create mode 100644 autopackage/CVS/Root create mode 100644 autopackage/dolcon.apspec.in create mode 100755 bootstrap create mode 100644 clients/CVS/Entries create mode 100644 clients/CVS/Repository create mode 100644 clients/CVS/Root create mode 100644 clients/Makefile.am create mode 100644 clients/gnome-trans-applet/CVS/Entries create mode 100644 clients/gnome-trans-applet/CVS/Repository create mode 100644 clients/gnome-trans-applet/CVS/Root create mode 100644 clients/gnome-trans-applet/Dolcon_Transferapplet_Factory.server.in create mode 100644 clients/gnome-trans-applet/Makefile.am create mode 100644 clients/gnome-trans-applet/conduit-dclib.c create mode 100644 clients/gnome-trans-applet/conduit-pipe.c create mode 100644 clients/gnome-trans-applet/conduit.c create mode 100644 clients/gnome-trans-applet/conduit.h create mode 100755 clients/gnome-trans-applet/dctrmon create mode 100644 clients/gnome-trans-applet/dolcon-trans-applet.c create mode 100644 clients/gtk2/CVS/Entries create mode 100644 clients/gtk2/CVS/Repository create mode 100644 clients/gtk2/CVS/Root create mode 100644 clients/gtk2/Makefile.am create mode 100644 clients/gtk2/emacs-local create mode 100644 clients/gtk2/inpdialog.desc create mode 100644 clients/gtk2/main.c create mode 100644 clients/gtk2/mainwnd.desc create mode 100755 clients/gtk2/makegdesc create mode 100644 clients/gtk2/pref.desc create mode 100644 clients/gtk2/progressbar.c create mode 100644 clients/gtk2/progressbar.h create mode 100644 clients/hellodolda.jpg create mode 100644 clients/test.c create mode 100644 config/CVS/Entries create mode 100644 config/CVS/Repository create mode 100644 config/CVS/Root create mode 100644 config/Makefile.am create mode 100644 config/dc-filter create mode 100755 config/dc-filtercmd create mode 100644 config/doldacond.conf create mode 100644 config/locktouch.c create mode 100644 config/speedrec.c create mode 100644 configure.in create mode 100644 daemon/CVS/Entries create mode 100644 daemon/CVS/Repository create mode 100644 daemon/CVS/Root create mode 100644 daemon/Makefile.am create mode 100644 daemon/auth-krb5.c create mode 100644 daemon/auth-pam.c create mode 100644 daemon/auth.c create mode 100644 daemon/auth.h create mode 100644 daemon/client.c create mode 100644 daemon/client.h create mode 100644 daemon/conf.c create mode 100644 daemon/conf.h create mode 100644 daemon/emacs-local create mode 100644 daemon/filenet.c create mode 100644 daemon/filenet.h create mode 100644 daemon/fnet-dc.c create mode 100644 daemon/log.c create mode 100644 daemon/log.h create mode 100644 daemon/main.c create mode 100644 daemon/module.h create mode 100644 daemon/net.c create mode 100644 daemon/net.h create mode 100644 daemon/search.c create mode 100644 daemon/search.h create mode 100644 daemon/sysevents.h create mode 100644 daemon/tiger.c create mode 100644 daemon/tiger.h create mode 100644 daemon/transfer.c create mode 100644 daemon/transfer.h create mode 100644 daemon/ui.c create mode 100644 daemon/uiretref create mode 100644 daemon/utils.c create mode 100644 daemon/utils.h create mode 100644 include/CVS/Entries create mode 100644 include/CVS/Repository create mode 100644 include/CVS/Root create mode 100644 include/Makefile.am create mode 100644 include/doldaconnect/CVS/Entries create mode 100644 include/doldaconnect/CVS/Repository create mode 100644 include/doldaconnect/CVS/Root create mode 100644 include/doldaconnect/uilib.h create mode 100644 include/doldaconnect/uimisc.h create mode 100644 include/doldaconnect/utils.h create mode 100644 lib/CVS/Entries create mode 100644 lib/CVS/Repository create mode 100644 lib/CVS/Root create mode 100644 lib/Makefile.am create mode 100644 lib/guile/CVS/Entries create mode 100644 lib/guile/CVS/Repository create mode 100644 lib/guile/CVS/Root create mode 100644 lib/guile/Makefile.am create mode 100755 lib/guile/autodl create mode 100755 lib/guile/chatlog create mode 100644 lib/guile/dolcon-guile.c create mode 100644 lib/guile/dolcon/CVS/Entries create mode 100644 lib/guile/dolcon/CVS/Repository create mode 100644 lib/guile/dolcon/CVS/Root create mode 100644 lib/guile/dolcon/Makefile.am create mode 100644 lib/guile/dolcon/ui.scm create mode 100644 lib/guile/dolcon/util.scm create mode 100644 lib/initcmds.h create mode 100755 lib/makecmds create mode 100644 lib/uicmds create mode 100644 lib/uilib.c create mode 100644 lib/uilib.h create mode 100644 lib/uimisc.c create mode 100644 lib/utils.c create mode 100644 po/CVS/Entries create mode 100644 po/CVS/Repository create mode 100644 po/CVS/Root create mode 100644 po/ChangeLog create mode 100644 po/LINGUAS create mode 100644 po/Makevars create mode 100644 po/POTFILES.in create mode 100644 po/sv.po diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..d17f040 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Maintainer, developer, documenter, etc.: +Fredrik Tolf diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..60549be --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + 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) 19yy + + 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) 19yy 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. diff --git a/CVS/Entries b/CVS/Entries new file mode 100644 index 0000000..2a2ae7f --- /dev/null +++ b/CVS/Entries @@ -0,0 +1,16 @@ +/AUTHORS/1.1.1.1/Tue May 11 15:45:57 2004// +/COPYING/1.1.1.1/Tue May 11 15:46:34 2004// +/ChangeLog/1.4/Sat Jul 9 03:42:47 2005// +/INSTALL/1.1.1.1/Tue May 11 15:46:34 2004// +/Makefile.am/1.4/Sat Jul 9 03:38:28 2005// +/NEWS/1.1.1.1/Tue May 11 15:46:45 2004// +/README/1.1.1.1/Tue May 11 15:46:45 2004// +/bootstrap/1.2/Sat Jul 9 03:17:49 2005// +/configure.in/1.19/Sun Oct 9 15:28:57 2005// +D/autopackage//// +D/clients//// +D/config//// +D/daemon//// +D/include//// +D/lib//// +D/po//// diff --git a/CVS/Repository b/CVS/Repository new file mode 100644 index 0000000..48b3e60 --- /dev/null +++ b/CVS/Repository @@ -0,0 +1 @@ +doldaconnect diff --git a/CVS/Root b/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..d4e4aa6 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,62 @@ +2005-07-09 Fredrik Tolf + + * Cleaned up CVS heavily. + +2004-08-13 gettextize + + * Makefile.am (SUBDIRS): Add m4. + (ACLOCAL_AMFLAGS): New variable. + (EXTRA_DIST): New variable. + * configure.in (AC_OUTPUT): Add po/Makefile.in, + +2004-05-11 dolda2000 + + * utils.h: Removed the format warnings, since they didn't work with %N. + + * transfer.h, transfer.c, fnet-dc.c: Transfer system rewrite. + + * net.c, net.h, sysevents.h, transfer.c, transfer.h, utils.c: + Initial import. + + * net.c, net.h, sysevents.h, transfer.c, transfer.h, utils.c: New file. + + * filenet.h, log.c, module.h: Initial import. + + * filenet.h, log.c, module.h: New file. + + * conf.h, fnet-dc.c, log.h: Initial import. + + * conf.h, fnet-dc.c, log.h: New file. + + * client.c, client.h, conf.c, filenet.c: Initial import. + + * client.c, client.h, conf.c, filenet.c: New file. + + * auth-pam.c: Initial import. + + * auth-pam.c: New file. + + * auth.c, auth.h: Initial import. + + * auth.c, auth.h: New file. + + * Makefile: Initial import. + + * Makefile: New file. + + * Makefile.in: Initial import. + + * Makefile.in: New file. + + * ui.c, uiretref, utils.h: Initial import. + + * ui.c, uiretref, utils.h: New file. + + * main.c: Initial import. + + * main.c: New file. + + * Makefile.am, emacs-local: Initial import. + + * Makefile.am, emacs-local: New file. + diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..b42a17a --- /dev/null +++ b/INSTALL @@ -0,0 +1,182 @@ +Basic Installation +================== + + These are generic installation instructions. + + The `configure' shell script attempts to guess correct values for +various system-dependent variables used during compilation. It uses +those values to create a `Makefile' in each directory of the package. +It may also create one or more `.h' files containing system-dependent +definitions. Finally, it creates a shell script `config.status' that +you can run in the future to recreate the current configuration, a file +`config.cache' that saves the results of its tests to speed up +reconfiguring, and a file `config.log' containing compiler output +(useful mainly for debugging `configure'). + + If you need to do unusual things to compile the package, please try +to figure out how `configure' could check whether to do them, and mail +diffs or instructions to the address given in the `README' so they can +be considered for the next release. If at some point `config.cache' +contains results you don't want to keep, you may remove or edit it. + + The file `configure.in' is used to create `configure' by a program +called `autoconf'. You only need `configure.in' if you want to change +it or regenerate `configure' using a newer version of `autoconf'. + +The simplest way to compile this package is: + + 1. `cd' to the directory containing the package's source code and type + `./configure' to configure the package for your system. If you're + using `csh' on an old version of System V, you might need to type + `sh ./configure' instead to prevent `csh' from trying to execute + `configure' itself. + + Running `configure' takes awhile. While running, it prints some + messages telling which features it is checking for. + + 2. Type `make' to compile the package. + + 3. Optionally, type `make check' to run any self-tests that come with + the package. + + 4. Type `make install' to install the programs and any data files and + documentation. + + 5. You can remove the program binaries and object files from the + source code directory by typing `make clean'. To also remove the + files that `configure' created (so you can compile the package for + a different kind of computer), type `make distclean'. There is + also a `make maintainer-clean' target, but that is intended mainly + for the package's developers. If you use it, you may have to get + all sorts of other programs in order to regenerate files that came + with the distribution. + +Compilers and Options +===================== + + Some systems require unusual options for compilation or linking that +the `configure' script does not know about. You can give `configure' +initial values for variables by setting them in the environment. Using +a Bourne-compatible shell, you can do that on the command line like +this: + CC=c89 CFLAGS=-O2 LIBS=-lposix ./configure + +Or on systems that have the `env' program, you can do it like this: + env CPPFLAGS=-I/usr/local/include LDFLAGS=-s ./configure + +Compiling For Multiple Architectures +==================================== + + You can compile the package for more than one kind of computer at the +same time, by placing the object files for each architecture in their +own directory. To do this, you must use a version of `make' that +supports the `VPATH' variable, such as GNU `make'. `cd' to the +directory where you want the object files and executables to go and run +the `configure' script. `configure' automatically checks for the +source code in the directory that `configure' is in and in `..'. + + If you have to use a `make' that does not supports the `VPATH' +variable, you have to compile the package for one architecture at a time +in the source code directory. After you have installed the package for +one architecture, use `make distclean' before reconfiguring for another +architecture. + +Installation Names +================== + + By default, `make install' will install the package's files in +`/usr/local/bin', `/usr/local/man', etc. You can specify an +installation prefix other than `/usr/local' by giving `configure' the +option `--prefix=PATH'. + + You can specify separate installation prefixes for +architecture-specific files and architecture-independent files. If you +give `configure' the option `--exec-prefix=PATH', the package will use +PATH as the prefix for installing programs and libraries. +Documentation and other data files will still use the regular prefix. + + In addition, if you use an unusual directory layout you can give +options like `--bindir=PATH' to specify different values for particular +kinds of files. Run `configure --help' for a list of the directories +you can set and what kinds of files go in them. + + If the package supports it, you can cause programs to be installed +with an extra prefix or suffix on their names by giving `configure' the +option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'. + +Optional Features +================= + + Some packages pay attention to `--enable-FEATURE' options to +`configure', where FEATURE indicates an optional part of the package. +They may also pay attention to `--with-PACKAGE' options, where PACKAGE +is something like `gnu-as' or `x' (for the X Window System). The +`README' should mention any `--enable-' and `--with-' options that the +package recognizes. + + For packages that use the X Window System, `configure' can usually +find the X include and library files automatically, but if it doesn't, +you can use the `configure' options `--x-includes=DIR' and +`--x-libraries=DIR' to specify their locations. + +Specifying the System Type +========================== + + There may be some features `configure' can not figure out +automatically, but needs to determine by the type of host the package +will run on. Usually `configure' can figure that out, but if it prints +a message saying it can not guess the host type, give it the +`--host=TYPE' option. TYPE can either be a short name for the system +type, such as `sun4', or a canonical name with three fields: + CPU-COMPANY-SYSTEM + +See the file `config.sub' for the possible values of each field. If +`config.sub' isn't included in this package, then this package doesn't +need to know the host type. + + If you are building compiler tools for cross-compiling, you can also +use the `--target=TYPE' option to select the type of system they will +produce code for and the `--build=TYPE' option to select the type of +system on which you are compiling the package. + +Sharing Defaults +================ + + If you want to set default values for `configure' scripts to share, +you can create a site shell script called `config.site' that gives +default values for variables like `CC', `cache_file', and `prefix'. +`configure' looks for `PREFIX/share/config.site' if it exists, then +`PREFIX/etc/config.site' if it exists. Or, you can set the +`CONFIG_SITE' environment variable to the location of the site script. +A warning: not all `configure' scripts look for a site script. + +Operation Controls +================== + + `configure' recognizes the following options to control how it +operates. + +`--cache-file=FILE' + Use and save the results of the tests in FILE instead of + `./config.cache'. Set FILE to `/dev/null' to disable caching, for + debugging `configure'. + +`--help' + Print a summary of the options to `configure', and exit. + +`--quiet' +`--silent' +`-q' + Do not print messages saying which checks are being made. To + suppress all normal output, redirect it to `/dev/null' (any error + messages will still be shown). + +`--srcdir=DIR' + Look for the package's source code in directory DIR. Usually + `configure' can determine that directory automatically. + +`--version' + Print the version of Autoconf used to generate the `configure' + script, and exit. + +`configure' also accepts some other, not widely useful, options. diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..a08c847 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,5 @@ +SUBDIRS= daemon lib include clients po config + +ACLOCAL_AMFLAGS = -I m4 + +EXTRA_DIST = config.rpath diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..e69de29 diff --git a/README b/README new file mode 100644 index 0000000..e69de29 diff --git a/autopackage/CVS/Entries b/autopackage/CVS/Entries new file mode 100644 index 0000000..b5479b2 --- /dev/null +++ b/autopackage/CVS/Entries @@ -0,0 +1,2 @@ +/dolcon.apspec.in/1.2/Tue Jul 12 02:05:28 2005// +D diff --git a/autopackage/CVS/Repository b/autopackage/CVS/Repository new file mode 100644 index 0000000..1564dad --- /dev/null +++ b/autopackage/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/autopackage diff --git a/autopackage/CVS/Root b/autopackage/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/autopackage/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/autopackage/dolcon.apspec.in b/autopackage/dolcon.apspec.in new file mode 100644 index 0000000..0fbfbf8 --- /dev/null +++ b/autopackage/dolcon.apspec.in @@ -0,0 +1,48 @@ +# -*-shell-script-*- + +[Meta] +RootName: @dolda2000.com/~fredrik/doldaconnect/dolcon:$SOFTWAREVERSION +DisplayName: Dolda Connect GTK 2 user interface +ShortName: doldaconnect-gtk +Maintainer: Fredrik Tolf +Packager: Fredrik Tolf +Summary: A user interface module for Dolda Connect using GTK 2. +URL: http://www.dolda2000.com/~fredrik/doldaconnect/ +License: GNU General Public License, Version 2 +SoftwareVersion: @VERSION@ +AutopackageTarget: 1.0 + +# Only uncomment InterfaceVersion if your package exposes interfaces to other software, +# for instance if it includes DSOs or python/perl modules. See the developer guide for more info, +# or ask on autopackage-dev if you don't understand interface versioning in autopackage. +# +# InterfaceVersion: 0.0 + +[Description] +This is a user interface program for Dolda Connect. It connects to the +Dolda Connect daemon and lets a user control it. This user interface +is written with GTK 2. + +Note that this program does not share files or anything of the sort -- +it is the daemon that does that. This program only controls the +daemon. + +[BuildPrepare] +prepareBuild --enable-gtk2ui --disable-gnomeapplet + +[BuildUnprepare] +unprepareBuild + +[Imports] +echo '*' | import + +[Prepare] +require @gtk.org/gtk 2.0 +require @dolda2000.com/~fredrik/doldaconnect/dcuilib + +[Install] +installExe bin/dolcon +installLocale share/locale + +[Uninstall] +uninstallFromLog diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..1afe2dd --- /dev/null +++ b/bootstrap @@ -0,0 +1,3 @@ +#! /bin/sh + +autopoint && aclocal$AUTOMAKE_VERSION -I m4 && autoheader && libtoolize --copy --force && automake$AUTOMAKE_VERSION --add-missing --copy && autoconf diff --git a/clients/CVS/Entries b/clients/CVS/Entries new file mode 100644 index 0000000..fcfb102 --- /dev/null +++ b/clients/CVS/Entries @@ -0,0 +1,5 @@ +/Makefile.am/1.6/Thu Dec 30 02:47:05 2004// +/hellodolda.jpg/1.1/Sun Dec 26 23:48:43 2004/-ko/ +/test.c/1.1.1.1/Tue May 11 15:46:45 2004// +D/gnome-trans-applet//// +D/gtk2//// diff --git a/clients/CVS/Repository b/clients/CVS/Repository new file mode 100644 index 0000000..e46c3bb --- /dev/null +++ b/clients/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/clients diff --git a/clients/CVS/Root b/clients/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/clients/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/clients/Makefile.am b/clients/Makefile.am new file mode 100644 index 0000000..aca20d2 --- /dev/null +++ b/clients/Makefile.am @@ -0,0 +1,13 @@ +SUBDIRS=@clients@ +DIST_SUBDIRS=gtk2 gnome-trans-applet +EXTRA_DIST=hellodolda.jpg + +iconsdir = $(datadir)/pixmaps +icons_DATA = hellodolda.jpg + +noinst_PROGRAMS=test + +test_SOURCES=test.c + +test_LDADD=$(top_srcdir)/lib/libdcui.la +AM_CPPFLAGS=-I$(top_srcdir)/include diff --git a/clients/gnome-trans-applet/CVS/Entries b/clients/gnome-trans-applet/CVS/Entries new file mode 100644 index 0000000..8beace3 --- /dev/null +++ b/clients/gnome-trans-applet/CVS/Entries @@ -0,0 +1,9 @@ +/Dolcon_Transferapplet_Factory.server.in/1.2/Thu Dec 30 01:11:03 2004// +/Makefile.am/1.5/Sat Jan 1 17:39:45 2005// +/conduit-dclib.c/1.4/Tue May 10 00:20:16 2005// +/conduit-pipe.c/1.4/Fri Dec 31 12:35:38 2004// +/conduit.c/1.3/Thu Dec 30 02:30:49 2004// +/conduit.h/1.4/Sat Jan 1 17:39:11 2005// +/dctrmon/1.2/Mon Jan 24 12:07:16 2005// +/dolcon-trans-applet.c/1.5/Tue Oct 11 20:23:48 2005// +D diff --git a/clients/gnome-trans-applet/CVS/Repository b/clients/gnome-trans-applet/CVS/Repository new file mode 100644 index 0000000..4ebbf5a --- /dev/null +++ b/clients/gnome-trans-applet/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/clients/gnome-trans-applet diff --git a/clients/gnome-trans-applet/CVS/Root b/clients/gnome-trans-applet/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/clients/gnome-trans-applet/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/clients/gnome-trans-applet/Dolcon_Transferapplet_Factory.server.in b/clients/gnome-trans-applet/Dolcon_Transferapplet_Factory.server.in new file mode 100644 index 0000000..b058242 --- /dev/null +++ b/clients/gnome-trans-applet/Dolcon_Transferapplet_Factory.server.in @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/clients/gnome-trans-applet/Makefile.am b/clients/gnome-trans-applet/Makefile.am new file mode 100644 index 0000000..c73f993 --- /dev/null +++ b/clients/gnome-trans-applet/Makefile.am @@ -0,0 +1,22 @@ +libexec_PROGRAMS=dolcon-trans-applet + +dolcon_trans_applet_SOURCES= dolcon-trans-applet.c \ + conduit-pipe.c \ + conduit-dclib.c \ + conduit.c \ + conduit.h + +localedir=$(datadir)/locale +dolcon_trans_applet_LDFLAGS=$(shell pkg-config --libs libpanelapplet-2.0) +dolcon_trans_applet_LDADD=$(top_srcdir)/lib/libdcui.la +dolcon_trans_applet_CPPFLAGS=$(shell pkg-config --cflags libpanelapplet-2.0) -DLOCALEDIR=\"$(localedir)\" + +BUILT_SOURCES=Dolcon_Transferapplet_Factory.server + +serverdir=$(libdir)/bonobo/servers +server_DATA=Dolcon_Transferapplet_Factory.server + +EXTRA_DIST=Dolcon_Transferapplet_Factory.server.in + +%.server: %.server.in + sed -e "s|\@LIBEXECDIR\@|$(libexecdir)|" $< > $@ diff --git a/clients/gnome-trans-applet/conduit-dclib.c b/clients/gnome-trans-applet/conduit-dclib.c new file mode 100644 index 0000000..afc90c3 --- /dev/null +++ b/clients/gnome-trans-applet/conduit-dclib.c @@ -0,0 +1,314 @@ +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include "conduit.h" + +struct data +{ + int fd; + int gdkread, gdkwrite; +}; + +struct dtdata +{ + struct conduit *conduit; + struct transfer *ct; + char *tag; + int realtag; +}; + +static struct conduit *inuse = NULL; + +static void dcfdcb(struct conduit *conduit, int fd, GdkInputCondition condition); + +static void updatewrite(struct conduit *conduit) +{ + struct data *data; + + data = conduit->cdata; + if(data->fd < 0) + return; + if(dc_wantwrite()) + { + if(data->gdkwrite == -1) + data->gdkwrite = gdk_input_add(data->fd, GDK_INPUT_WRITE, (void (*)(gpointer, int, GdkInputCondition))dcfdcb, conduit); + } else { + if(data->gdkwrite != -1) + { + gdk_input_remove(data->gdkwrite); + data->gdkwrite = -1; + } + } +} + +static void disconnected(struct conduit *conduit) +{ + struct data *data; + + data = conduit->cdata; + if(inuse == conduit) + inuse = NULL; + if(data->gdkread != -1) + { + gdk_input_remove(data->gdkread); + data->gdkread = -1; + } + if(data->gdkwrite != -1) + { + gdk_input_remove(data->gdkwrite); + data->gdkwrite = -1; + } + data->fd = -1; + conddisconn(conduit); +} + +static int noconv(int type, wchar_t *text, char **resp, void *data) +{ + return(1); +} + +static char *gettag(struct dc_transfer *dt) +{ + char *mbspath, *p, *buf; + + if(dt->path == NULL) + return(NULL); + if((mbspath = icwcstombs(dt->path, "UTF-8")) == NULL) + return(NULL); + /* XXX: Achtung! Too DC-specific! */ + if((p = strrchr(mbspath, '\\')) == NULL) + p = mbspath; + else + p++; + buf = sstrdup(p); + free(mbspath); + return(buf); +} + +static void dtfreecb(struct dc_transfer *dt) +{ + struct dtdata *dtd; + + if((dtd = dt->udata) == NULL) + return; + if(dtd->ct != NULL) + freetransfer(dtd->ct); + if(dtd->tag != NULL) + free(dtd->tag); + free(dtd); +} + +static int lstrargcb(struct dc_response *resp) +{ + struct dc_transfer *dt; + struct dtdata *dtd; + struct dc_intresp *ires; + + dt = resp->data; + dtd = dt->udata; + if(resp->code == 200) + { + while((dtd->tag == NULL) && ((ires = dc_interpret(resp)) != NULL)) + { + if(!wcscmp(ires->argv[0].val.str, L"tag")) + { + dtd->realtag = 1; + dtd->tag = icwcstombs(ires->argv[1].val.str, "UTF-8"); + } + dc_freeires(ires); + } + } + if(dtd->tag == NULL) + dtd->tag = gettag(dt); + dtd->ct = newtransfer(dtd->conduit, dtd->tag, dt->size, dt->curpos); + return(1); +} + +static void inittrans(struct conduit *conduit, struct dc_transfer *dt) +{ + struct dtdata *dtd; + + dtd = smalloc(sizeof(*dtd)); + memset(dtd, 0, sizeof(*dtd)); + dtd->conduit = conduit; + dt->udata = dtd; + dt->destroycb = dtfreecb; + dc_queuecmd(lstrargcb, dt, L"lstrarg", L"%%i", dt->id, NULL); +} + +static void trlistcb(int resp, struct conduit *conduit) +{ + struct data *data; + struct dc_transfer *dt; + + data = conduit->cdata; + if(resp != 200) + return; + for(dt = dc_transfers; dt != NULL; dt = dt->next) + { + if(dt->dir != DC_TRNSD_DOWN) + continue; + inittrans(conduit, dt); + } +} + +static void logincb(int err, wchar_t *reason, struct conduit *conduit) +{ + struct data *data; + + data = conduit->cdata; + if(err != DC_LOGIN_ERR_SUCCESS) + { + dc_disconnect(); + disconnected(conduit); + return; + } + condconnected(conduit); + dc_gettrlistasync((void (*)(int, void *))trlistcb, conduit); + dc_queuecmd(NULL, NULL, L"notify", L"trans:act", L"on", L"trans:prog", L"on", NULL); +} + +static void dcfdcb(struct conduit *conduit, int fd, GdkInputCondition condition) +{ + struct data *data; + struct dc_response *resp; + struct dc_intresp *ires; + struct dc_transfer *dt; + struct dtdata *dtd; + + data = conduit->cdata; + if(((condition & GDK_INPUT_READ) && dc_handleread()) || ((condition & GDK_INPUT_WRITE) && dc_handlewrite())) + { + disconnected(conduit); + return; + } + while((resp = dc_getresp()) != NULL) + { + if(!wcscmp(resp->cmdname, L".connect")) + { + if(resp->code == 200) + { + dc_loginasync(NULL, 1, noconv, (void (*)(int, wchar_t *, void *))logincb, conduit); + } else { + dc_disconnect(); + disconnected(conduit); + } + } else if(!wcscmp(resp->cmdname, L".notify")) { + dc_uimisc_handlenotify(resp); + switch(resp->code) + { + case 610: + if((ires = dc_interpret(resp)) != NULL) + { + if((dt = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + if(dt->dir == DC_TRNSD_DOWN) + inittrans(conduit, dt); + } + dc_freeires(ires); + } + break; + case 613: + if((ires = dc_interpret(resp)) != NULL) + { + if((dt = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + if(((dtd = dt->udata) != NULL) && (dtd->ct != NULL)) + { + if(dtd->ct->size != dt->size) + transfersetsize(dtd->ct, dt->size); + } + } + dc_freeires(ires); + } + break; + case 615: + if((ires = dc_interpret(resp)) != NULL) + { + if((dt = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + if(((dtd = dt->udata) != NULL) && (dtd->ct != NULL)) + { + if(dtd->ct->pos != dt->curpos) + transfersetpos(dtd->ct, dt->curpos); + } + } + dc_freeires(ires); + } + break; + } + } + dc_freeresp(resp); + } + updatewrite(conduit); +} + +static int init(struct conduit *conduit) +{ + static int inited = 0; + struct data *data; + + if(!inited) + { + dc_init(); + inited = 1; + } + data = smalloc(sizeof(*data)); + memset(data, 0, sizeof(*data)); + data->fd = -1; + data->gdkread = data->gdkwrite = -1; + conduit->cdata = data; + return(0); +} + +static int connect(struct conduit *conduit) +{ + struct data *data; + char *host; + + data = conduit->cdata; + if(inuse != NULL) + return(-1); + if((host = getenv("DCSERVER")) == NULL) + host = "localhost"; + if((data->fd = dc_connect(host, -1)) < 0) + return(-1); + data->gdkread = gdk_input_add(data->fd, GDK_INPUT_READ, (void (*)(gpointer, int, GdkInputCondition))dcfdcb, conduit); + updatewrite(conduit); + inuse = conduit; + return(0); +} + +static void destroy(struct conduit *conduit) +{ + struct data *data; + + data = conduit->cdata; + if(data->gdkread != -1) + gdk_input_remove(data->gdkread); + if(data->gdkwrite != -1) + gdk_input_remove(data->gdkwrite); + if(data->fd >= 0) + dc_disconnect(); + if(inuse == conduit) + inuse = NULL; + free(data); +} + +static struct conduitiface st_conduit_dclib = +{ + .init = init, + .connect = connect, + .destroy = destroy, +}; + +struct conduitiface *conduit_dclib = &st_conduit_dclib; diff --git a/clients/gnome-trans-applet/conduit-pipe.c b/clients/gnome-trans-applet/conduit-pipe.c new file mode 100644 index 0000000..c58f4b4 --- /dev/null +++ b/clients/gnome-trans-applet/conduit-pipe.c @@ -0,0 +1,190 @@ +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "conduit.h" + +#define SUBPROCCMD "ksu fredrik -q -e /home/fredrik/bin/dctrmon" + +struct data +{ + pid_t subproc; + int fd; + int gdktag; + char *inbuf; + size_t inbufsize, inbufdata; +}; + +static void pipefdcb(struct conduit *conduit, int fd, GdkInputCondition condition) +{ + struct data *data; + int ret; + char *p, *p2, *cmd; + char **args; + size_t argssize, argsdata; + struct transfer *transfer; + + data = (struct data *)conduit->cdata; + if(conduit->state == CNDS_SYN) + condconnected(conduit); + sizebuf2(data->inbuf, data->inbufdata + 80, 1); + ret = read(data->fd, data->inbuf + data->inbufdata, data->inbufsize - data->inbufdata); + if(ret < 0) + { + if((errno == EINTR) || (errno == EAGAIN)) /* Shouldn't happen, but, oh well... */ + return; + perror("conduit-pipe: read"); + gdk_input_remove(data->gdktag); + close(data->fd); + kill(-data->subproc, SIGHUP); + data->gdktag = -1; + data->fd = -1; + data->subproc = 0; + conddisconn(conduit); + } + if(ret == 0) + { + gdk_input_remove(data->gdktag); + close(data->fd); + kill(-data->subproc, SIGHUP); + data->gdktag = -1; + data->fd = -1; + data->subproc = 0; + conddisconn(conduit); + } + data->inbufdata += ret; + while((p = memchr(data->inbuf, '\n', data->inbufdata)) != NULL) + { + *p = 0; + cmd = sstrdup(data->inbuf); + memmove(data->inbuf, p + 1, data->inbufdata -= (p - data->inbuf) + 1); + args = NULL; + argssize = argsdata = 0; + p = cmd; + do + { + if((p2 = strchr(p, '\t')) != NULL) + *(p2++) = 0; + addtobuf(args, p); + p = p2; + } while(p2 != NULL); + if(!strcmp(args[0], "N") && (argsdata >= 4)) + { + transfer = newtransfer(conduit, args[1], atoi(args[2]), atoi(args[3])); + } + if(!strcmp(args[0], "D") && (argsdata >= 2)) + { + if((transfer = findtransferbytag(conduit, args[1])) != NULL) + freetransfer(transfer); + } + if(!strcmp(args[0], "S") && (argsdata >= 3)) + { + if((transfer = findtransferbytag(conduit, args[1])) != NULL) + transfersetsize(transfer, atoi(args[2])); + } + if(!strcmp(args[0], "P") && (argsdata >= 3)) + { + if((transfer = findtransferbytag(conduit, args[1])) != NULL) + transfersetpos(transfer, atoi(args[2])); + } + free(args); + free(cmd); + } +} + +static int init(struct conduit *conduit) +{ + static int inited = 0; + struct data *data; + + if(!inited) + { + signal(SIGCHLD, SIG_IGN); + inited = 1; + } + data = smalloc(sizeof(*data)); + memset(data, 0, sizeof(*data)); + data->fd = -1; + data->inbuf = NULL; + data->inbufsize = data->inbufdata = 0; + data->gdktag = -1; + conduit->cdata = data; + return(0); +} + +static int connect(struct conduit *conduit) +{ + struct data *data; + pid_t pid; + int pfd[2]; + + data = conduit->cdata; + if(pipe(pfd)) + return(-1); + if((pid = fork()) < 0) + { + close(pfd[0]); + close(pfd[1]); + return(-1); + } + if(!pid) + { + int devnull; + + setpgrp(); + if((devnull = open("/dev/null", O_RDWR)) < 0) + exit(127); + close(pfd[0]); + dup2(pfd[1], 1); + close(pfd[1]); + dup2(devnull, 0); + close(devnull); + /* Leave stderr as is */ + execl("/bin/sh", "sh", "-c", SUBPROCCMD, NULL); + exit(127); + } + close(pfd[1]); + fcntl(pfd[0], F_SETFL, fcntl(pfd[0], F_GETFL) | O_NONBLOCK); + data->subproc = pid; + data->fd = pfd[0]; + data->gdktag = gdk_input_add(pfd[0], GDK_INPUT_READ, (void (*)(gpointer, gint, GdkInputCondition))pipefdcb, conduit); + data->inbufdata = 0; + return(0); +} + +static void destroy(struct conduit *conduit) +{ + struct data *data; + + data = conduit->cdata; + if(data == NULL) + return; + if(data->gdktag >= 0) + gdk_input_remove(data->gdktag); + if(data->subproc > 0) + kill(-data->subproc, SIGHUP); + if(data->fd >= 0) + close(data->fd); + if(data->inbuf != NULL) + free(data->inbuf); + free(data); + conduit->cdata = NULL; +} + +static struct conduitiface st_conduit_pipe = +{ + .init = init, + .connect = connect, + .destroy = destroy, +}; + +struct conduitiface *conduit_pipe = &st_conduit_pipe; diff --git a/clients/gnome-trans-applet/conduit.c b/clients/gnome-trans-applet/conduit.c new file mode 100644 index 0000000..5959a27 --- /dev/null +++ b/clients/gnome-trans-applet/conduit.c @@ -0,0 +1,145 @@ +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include + +#include "conduit.h" + +void (*cb_condstate)(struct conduit *conduit, void *data) = NULL; +void (*cb_trsize)(struct transfer *transfer, void *data) = NULL; +void (*cb_trpos)(struct transfer *transfer, void *data) = NULL; +void (*cb_trnew)(struct transfer *transfer, void *data) = NULL; +void (*cb_trfree)(struct transfer *transfer, void *data) = NULL; + +struct transfer *findtransferbytag(struct conduit *conduit, char *tag) +{ + struct transfer *transfer; + + for(transfer = conduit->transfers; transfer != NULL; transfer = transfer->next) + { + if((transfer->tag != NULL) && !strcmp(transfer->tag, tag)) + break; + } + return(transfer); +} + +void transfersetsize(struct transfer *transfer, int size) +{ + transfer->size = size; + cb_trsize(transfer, transfer->conduit->udata); +} + +void transfersetpos(struct transfer *transfer, int pos) +{ + transfer->pos = pos; + cb_trpos(transfer, transfer->conduit->udata); +} + +static gboolean trupdatetime(struct transfer *transfer) +{ + time_t now; + + if((transfer->size == -1) || (transfer->pos == -1)) + return(TRUE); + now = time(NULL); + if(now - transfer->ckptime >= 10) + { + transfer->cmptime = transfer->ckptime; + transfer->cmpsize = transfer->ckpsize; + transfer->ckptime = 0; + } + if(transfer->ckptime == 0) + { + transfer->ckptime = now; + transfer->ckpsize = transfer->pos; + } + return(TRUE); +} + +struct transfer *newtransfer(struct conduit *conduit, char *tag, int size, int pos) +{ + struct transfer *transfer; + + transfer = smalloc(sizeof(*transfer)); + memset(transfer, 0, sizeof(*transfer)); + if(tag != NULL) + transfer->tag = sstrdup(tag); + transfer->size = size; + transfer->pos = pos; + transfer->timeout = g_timeout_add(1000, (gboolean (*)(gpointer))trupdatetime, transfer); + transfer->next = conduit->transfers; + transfer->conduit = conduit; + if(conduit->transfers != NULL) + conduit->transfers->prev = transfer; + conduit->transfers = transfer; + cb_trnew(transfer, conduit->udata); + return(transfer); +} + +void freetransfer(struct transfer *transfer) +{ + if(transfer->next != NULL) + transfer->next->prev = transfer->prev; + if(transfer->prev != NULL) + transfer->prev->next = transfer->next; + if(transfer->conduit->transfers == transfer) + transfer->conduit->transfers = transfer->next; + cb_trfree(transfer, transfer->conduit->udata); + g_source_remove(transfer->timeout); + if(transfer->tag != NULL) + free(transfer->tag); + free(transfer); +} + +struct conduit *newconduit(struct conduitiface *iface, void *udata) +{ + struct conduit *conduit; + + conduit = smalloc(sizeof(*conduit)); + memset(conduit, 0, sizeof(*conduit)); + conduit->iface = iface; + conduit->udata = udata; + if(iface->init(conduit)) + { + free(conduit); + return(NULL); + } + return(conduit); +} + +void freeconduit(struct conduit *conduit) +{ + conduit->iface->destroy(conduit); + while(conduit->transfers != NULL) + freetransfer(conduit->transfers); + free(conduit); +} + +int condtryconn(struct conduit *conduit) +{ + if(conduit->state != CNDS_IDLE) + return(-1); + if(conduit->iface->connect(conduit)) + return(-1); + conduit->state = CNDS_SYN; + return(0); +} + +void conddisconn(struct conduit *conduit) +{ + while(conduit->transfers != NULL) + freetransfer(conduit->transfers); + conduit->state = CNDS_IDLE; + cb_condstate(conduit, conduit->udata); +} + +void condconnected(struct conduit *conduit) +{ + conduit->state = CNDS_EST; + cb_condstate(conduit, conduit->udata); +} diff --git a/clients/gnome-trans-applet/conduit.h b/clients/gnome-trans-applet/conduit.h new file mode 100644 index 0000000..2540960 --- /dev/null +++ b/clients/gnome-trans-applet/conduit.h @@ -0,0 +1,55 @@ +#ifndef _CONDUIT_H +#define _CONDUIT_H + +#include + +#define CNDS_IDLE 0 +#define CNDS_SYN 1 +#define CNDS_EST 2 + +struct transfer +{ + struct transfer *next, *prev; + struct conduit *conduit; + char *tag; /* UTF8 */ + int pos, size; + time_t cmptime, ckptime; + size_t cmpsize, ckpsize; + int timeout; +}; + +struct conduit +{ + struct transfer *transfers; + struct conduitiface *iface; + void *cdata, *udata; + int state; +}; + +struct conduitiface +{ + int (*init)(struct conduit *conduit); + int (*connect)(struct conduit *conduit); + void (*destroy)(struct conduit *conduit); +}; + +struct transfer *findtransferbytag(struct conduit *conduit, char *tag); +void transfersetsize(struct transfer *transfer, int size); +void transfersetpos(struct transfer *transfer, int pos); +struct transfer *newtransfer(struct conduit *conduit, char *tag, int size, int pos); +void freetransfer(struct transfer *transfer); +struct conduit *newconduit(struct conduitiface *iface, void *udata); +void freeconduit(struct conduit *conduit); +int condtryconn(struct conduit *conduit); +void conddisconn(struct conduit *conduit); +void condconnected(struct conduit *conduit); + +extern void (*cb_condstate)(struct conduit *conduit, void *data); +extern void (*cb_trsize)(struct transfer *transfer, void *data); +extern void (*cb_trpos)(struct transfer *transfer, void *data); +extern void (*cb_trnew)(struct transfer *transfer, void *data); +extern void (*cb_trfree)(struct transfer *transfer, void *data); +extern struct conduitiface *conduit_pipe; +extern struct conduitiface *conduit_dclib; + +#endif diff --git a/clients/gnome-trans-applet/dctrmon b/clients/gnome-trans-applet/dctrmon new file mode 100755 index 0000000..8b9e8cb --- /dev/null +++ b/clients/gnome-trans-applet/dctrmon @@ -0,0 +1,93 @@ +#!/usr/bin/guile \ +--debug -s +!# + +(use-modules (dolcon ui)) +(use-modules (ice-9 popen)) +(use-modules (ice-9 pretty-print)) + +(define (flush port) + (force-output port)) + +(define idlist '()) +(define filter '()) +(define (filtered tag filter) + (and (pair? filter) + (or (equal? (car filter) (substring tag 0 (min (string-length (car filter)) (string-length tag)))) + (filtered tag (cdr filter))))) +(catch 'system-error + (lambda () + (let ((port (open-input-file (string-append (getenv "HOME") "/.dctrmon-defines"))) (form #f)) + (while (begin (set! form (read port)) (not (eof-object? form))) + (primitive-eval form)))) + (lambda args + #f)) + + +(define krbcc (string-append "/tmp/krb5cc_dcmon_" (number->string (getuid)) "_XXXXXX")) +(close-port (mkstemp! krbcc)) +(setenv "KRB5CCNAME" (string-append "FILE:" krbcc)) +(sigaction SIGCHLD SIG_DFL) +(define pid (primitive-fork)) +(if (= pid 0) + (begin (execlp "kinit" "kinit" "-f" "-r" "10d" "-k" "-t" (string-append (getenv "HOME") "/.myprinc.keytab") (string-append (passwd:name (getpwuid (getuid))) "/dcview")) + (exit 1)) + (if (not (= (cdr (waitpid pid)) 0)) + (exit 1))) +(dc-c&l #f (getenv "DCSERVER") #t) +(delete-file krbcc) + +(dc-ecmd-assert 200 "notify" "all" "on") + +(display "C\n") + +(let ((resp (dc-ecmd-assert '(200 201) "lstrans"))) + (if (and resp (= (cdr (assoc 'code (dc-extract resp))) 200)) + (for-each (lambda (o) + (if (= (cadr o) 2) + (catch 'bad-return + (lambda () + (for-each (lambda (a) + (if (and (equal? (car a) "tag") (filtered (cadr a) filter)) + (begin + (display (string-append "N\t" (cadr a) "\t" (number->string (list-ref o 6)) "\t" (number->string (list-ref o 7)) "\n")) + (set! idlist (append idlist (list (cons (car o) (cadr a)))))))) + (dc-intall (dc-ecmd-assert 200 "lstrarg" (car o))))) + (lambda args #f)))) + (dc-intall resp)))) + +(flush (current-output-port)) + +(while #t + (dc-select 10000) + (while (let ((resp (dc-getresp))) + (if resp + (begin + (let* ((er (dc-extract resp)) (code (cdr (assoc 'code er))) (cmd (cdr (assoc 'cmd er))) (ir (dc-intresp resp))) + (if (equal? cmd ".notify") + (case code + ((610) + (let* ((id (car ir)) (ir2 (dc-intall (dc-ecmd-assert '(200 201) "lstrarg" id))) (tag (if (eq? (car ir2) '()) #f (assoc "tag" ir2)))) + (if (and tag (filtered (cadr tag) filter)) + (begin (display (string-append "N\t" (cadr tag) "\t-1\t-1\n")) + (flush (current-output-port)) + (set! idlist (append idlist (list (cons (car ir) (cadr tag))))))))) + ((613) + (let ((id (car ir))) + (if (assoc id idlist) + (begin (display (string-append "S\t" (cdr (assoc id idlist)) "\t" (number->string (cadr ir)) "\n")) + (flush (current-output-port)))))) + ((615) + (let ((id (car ir))) + (if (assoc id idlist) + (begin (display (string-append "P\t" (cdr (assoc id idlist)) "\t" (number->string (cadr ir)) "\n")) + (flush (current-output-port)))))) + ((617) + (let ((id (car ir))) + (if (assoc id idlist) + (begin (display (string-append "D\t" (cdr (assoc id idlist)) "\n")) + (flush (current-output-port))))))))) + #t) + #f)) #f)) + +(dc-disconnect) diff --git a/clients/gnome-trans-applet/dolcon-trans-applet.c b/clients/gnome-trans-applet/dolcon-trans-applet.c new file mode 100644 index 0000000..b09e529 --- /dev/null +++ b/clients/gnome-trans-applet/dolcon-trans-applet.c @@ -0,0 +1,229 @@ +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include + +#include "conduit.h" + +struct appletdata +{ + PanelApplet *applet; + GtkLabel *label; + GtkProgressBar *pbar; + GtkTooltips *tips; + gint tiptimeout; + struct conduit *conduit; + struct transfer *curdisplay; +}; + +static char *ctxtmenu = +"" +" " +" " +""; + +static void run_pref_dialog(BonoboUIComponent *uic, gpointer data, const char *cname) +{ +} + +static BonoboUIVerb ctxtmenuverbs[] = +{ + BONOBO_UI_VERB("dca_pref", run_pref_dialog), + BONOBO_UI_VERB_END +}; + +static gint reconncb(struct appletdata *data) +{ + condtryconn(data->conduit); + return(FALSE); +} + +static gboolean updatetip(struct appletdata *data) +{ + int diff, speed, left; + time_t now; + char buf[256]; + + if(data->curdisplay == NULL) + return(TRUE); + now = time(NULL); + if(data->curdisplay->cmptime == 0) + { + strcpy(buf, _("Calculating remaining time...")); + } else { + diff = data->curdisplay->pos - data->curdisplay->cmpsize; + speed = diff / (now - data->curdisplay->cmptime); + if(speed == 0) + { + strcpy(buf, _("Time left: Infinite (Transfer is standing still)")); + } else { + left = (data->curdisplay->size - data->curdisplay->pos) / speed; + sprintf(buf, _("Time left: %i:%02i"), left / 3600, (left / 60) % 60); + } + } + gtk_tooltips_set_tip(data->tips, GTK_WIDGET(data->applet), buf, NULL); + return(TRUE); +} + +static void update(struct appletdata *data) +{ + char buf[256]; + + switch(data->conduit->state) + { + case CNDS_IDLE: + gtk_progress_bar_set_text(data->pbar, _("Not connected")); + gtk_label_set_text(data->label, ""); + break; + case CNDS_SYN: + gtk_progress_bar_set_text(data->pbar, _("Connecting...")); + gtk_label_set_text(data->label, ""); + break; + case CNDS_EST: + if(data->conduit->transfers == NULL) + { + gtk_progress_bar_set_fraction(data->pbar, 0); + gtk_progress_bar_set_text(data->pbar, ""); + gtk_label_set_text(data->label, _("No transfers to display")); + } else if(data->curdisplay == NULL) { + gtk_progress_bar_set_fraction(data->pbar, 0); + gtk_progress_bar_set_text(data->pbar, ""); + gtk_label_set_text(data->label, _("No transfer selected")); + } else { + if((data->curdisplay->pos > 0) && (data->curdisplay->size > 0)) + { + sprintf(buf, "%'i/%'i", data->curdisplay->pos, data->curdisplay->size); + gtk_progress_bar_set_fraction(data->pbar, (double)data->curdisplay->pos / (double)data->curdisplay->size); + gtk_progress_bar_set_text(data->pbar, buf); + } else { + gtk_progress_bar_set_fraction(data->pbar, 0); + gtk_progress_bar_set_text(data->pbar, _("Initializing")); + } + gtk_label_set_text(data->label, data->curdisplay->tag); + } + break; + } +} + +static void trsize(struct transfer *transfer, struct appletdata *data) +{ + update(data); +} + +static void trpos(struct transfer *transfer, struct appletdata *data) +{ + update(data); +} + +static void trnew(struct transfer *transfer, struct appletdata *data) +{ + if(data->curdisplay == NULL) + data->curdisplay = transfer; + update(data); +} + +static void trfree(struct transfer *transfer, struct appletdata *data) +{ + if(data->curdisplay == transfer) + data->curdisplay = data->conduit->transfers; + update(data); +} + +static void condstate(struct conduit *conduit, struct appletdata *data) +{ + if(conduit->state == CNDS_IDLE) + g_timeout_add(10000, (gboolean (*)(gpointer))reconncb, data); + update(data); +} + +static void initcond(void) +{ + static int inited = 0; + + if(!inited) + { + cb_trsize = (void (*)(struct transfer *, void *))trsize; + cb_trpos = (void (*)(struct transfer *, void *))trpos; + cb_trnew = (void (*)(struct transfer *, void *))trnew; + cb_trfree = (void (*)(struct transfer *, void *))trfree; + cb_condstate = (void (*)(struct conduit *, void *))condstate; + inited = 1; + } +} + +static gboolean trview_applet_button_press(GtkWidget *widget, GdkEventButton *event, struct appletdata *data) +{ + if(event->button == 1) + { + if(data->curdisplay == NULL) + data->curdisplay = data->conduit->transfers; + else if(data->curdisplay->next == NULL) + data->curdisplay = data->conduit->transfers; + else + data->curdisplay = data->curdisplay->next; + update(data); + } + return(FALSE); +} + +static void trview_applet_destroy(GtkWidget *widget, struct appletdata *data) +{ + freeconduit(data->conduit); + g_source_remove(data->tiptimeout); + g_object_unref(data->applet); + g_object_unref(data->tips); + free(data); +} + +static gboolean trview_applet_fill(PanelApplet *applet, const gchar *iid, gpointer uudata) +{ + GtkWidget *hbox, *pbar, *label; + struct appletdata *data; + + initcond(); + if(strcmp(iid, "OAFIID:Dolcon_Transferapplet")) + return(FALSE); + + panel_applet_setup_menu(applet, ctxtmenu, ctxtmenuverbs, NULL); + + hbox = gtk_hbox_new(FALSE, 0); + label = gtk_label_new(""); + gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 5); + pbar = gtk_progress_bar_new(); + gtk_box_pack_start(GTK_BOX(hbox), pbar, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(applet), hbox); + gtk_widget_show_all(GTK_WIDGET(applet)); + + data = smalloc(sizeof(*data)); + memset(data, 0, sizeof(*data)); + g_object_ref(data->applet = applet); + data->conduit = newconduit(conduit_dclib, data); + data->pbar = GTK_PROGRESS_BAR(pbar); + g_object_ref(data->tips = gtk_tooltips_new()); + data->tiptimeout = g_timeout_add(500, (gboolean (*)(gpointer))updatetip, data); + data->label = GTK_LABEL(label); + + g_signal_connect(applet, "button-press-event", (GCallback)trview_applet_button_press, data); + g_signal_connect(applet, "destroy", (GCallback)trview_applet_destroy, data); + + condtryconn(data->conduit); + + update(data); + + return(TRUE); +} + +#define GETTEXT_PACKAGE PACKAGE +#define GNOMELOCALEDIR LOCALEDIR + +PANEL_APPLET_BONOBO_FACTORY("OAFIID:Dolcon_Transferapplet_Factory", + PANEL_TYPE_APPLET, + "Doldaconnect Transfer Viewer", + "0", + trview_applet_fill, + NULL); diff --git a/clients/gtk2/CVS/Entries b/clients/gtk2/CVS/Entries new file mode 100644 index 0000000..2282c03 --- /dev/null +++ b/clients/gtk2/CVS/Entries @@ -0,0 +1,10 @@ +/Makefile.am/1.11/Mon Oct 4 02:05:16 2004// +/emacs-local/1.1/Fri Aug 13 18:05:08 2004// +/inpdialog.desc/1.2/Sun Sep 26 03:18:56 2004// +/main.c/1.22/Mon Nov 15 08:34:25 2004// +/mainwnd.desc/1.14/Tue Oct 26 02:16:28 2004// +/makegdesc/1.9/Sun Oct 3 22:28:16 2004// +/pref.desc/1.2/Sun Sep 26 03:19:19 2004// +/progressbar.c/1.1/Thu Aug 5 00:22:11 2004// +/progressbar.h/1.1/Thu Aug 5 00:22:17 2004// +D diff --git a/clients/gtk2/CVS/Repository b/clients/gtk2/CVS/Repository new file mode 100644 index 0000000..26ff2c0 --- /dev/null +++ b/clients/gtk2/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/clients/gtk2 diff --git a/clients/gtk2/CVS/Root b/clients/gtk2/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/clients/gtk2/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/clients/gtk2/Makefile.am b/clients/gtk2/Makefile.am new file mode 100644 index 0000000..6afbb6f --- /dev/null +++ b/clients/gtk2/Makefile.am @@ -0,0 +1,21 @@ +bin_PROGRAMS=dolcon + +dolcon_SOURCES= main.c \ + progressbar.c \ + progressbar.h + +EXTRA_DIST=mainwnd.desc inpdialog.desc pref.desc makegdesc + +BUILT_SOURCES=mainwnd.gtk inpdialog.gtk pref.gtk + +main.c: mainwnd.gtk inpdialog.gtk + +localedir=$(datadir)/locale +dolcon_LDFLAGS=$(shell pkg-config --libs gtk+-2.0) +dolcon_LDADD=$(top_srcdir)/lib/libdcui.la +dolcon_CPPFLAGS=$(shell pkg-config --cflags gtk+-2.0) -DLOCALEDIR=\"$(localedir)\" + +%.gtk: %.desc makegdesc + cpp $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) $< | ./makegdesc >$@ + +mainwnd.desc: ../../config.h diff --git a/clients/gtk2/emacs-local b/clients/gtk2/emacs-local new file mode 100644 index 0000000..f7263a1 --- /dev/null +++ b/clients/gtk2/emacs-local @@ -0,0 +1,13 @@ +; -*-Lisp-*- + +; Use with: +; (add-hook 'find-file-hooks +; (lambda () +; (load (concat default-directory "emacs-local") t))) + +(if + (string-match "\\.[ch]$" (buffer-file-name (current-buffer))) + (progn + (make-local-variable 'compile-command) + (setq compile-command "make -k 'CFLAGS=-g -Wall'") +)) diff --git a/clients/gtk2/inpdialog.desc b/clients/gtk2/inpdialog.desc new file mode 100644 index 0000000..b441f33 --- /dev/null +++ b/clients/gtk2/inpdialog.desc @@ -0,0 +1,8 @@ +;prefix: inpdialog_ +:hbox + $simg stock: DIALOG_QUESTION size: DIALOG + :vbox + $lbl name: prompt label: " " var: y + $text name: entry var: y sig: activate + end +end diff --git a/clients/gtk2/main.c b/clients/gtk2/main.c new file mode 100644 index 0000000..66b1cce --- /dev/null +++ b/clients/gtk2/main.c @@ -0,0 +1,1815 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "progressbar.h" + + +struct fndata +{ + GtkTextBuffer *textbuf; +}; + +struct srchsize +{ + int size; + int num; + int slots; + double resptime; + GtkTreeRowReference *ref; +}; + +struct knownspeed +{ + char *userid; + int speed, seq; + time_t fetched; +}; + +GtkWidget *inpdialog; +GtkListStore *fnmodel, *ulmodel, *dlmodel, *pubhubmodel; +GtkTreeStore *srchmodel; +GtkTreeModelFilter *srchmodelfilter; +GtkTextTagTable *chattags; +int dcfd = -1, gdkread = -1, gdkwrite = -1; +int pubhubfd = -1, pubhubtag = -1, filterpubhub = 0; +int curchat = -1; +regex_t pubhubfilter; +pid_t pubhubproc = 0; +char *pubhubaddr = NULL; +char *connectas = NULL; +char *dcserver = NULL; +int autoconn = 0; +int srchautoupdate = 0; +int cursrch = -1, nextsrch = -1; +time_t srcheta; +struct srchsize *srchsizes = NULL; +struct knownspeed *knownspeeds = NULL; +int numsizes = 0, numspeeds = 0, ksqueryseq = -1, ksquerytag = -1; + +gboolean initdeath(GtkWidget *, gpointer); +void cb_main_connmenu_activate(GtkWidget *widget, gpointer data); +void cb_main_dconnmenu_activate(GtkWidget *widget, gpointer data); +void cb_main_prefmenu_activate(GtkWidget *widget, gpointer data); +void cb_main_sdmenu_activate(GtkWidget *widget, gpointer data); +void cb_inpdialog_entry_activate(GtkWidget *widget, gpointer data); +void cb_main_fnaddr_activate(GtkWidget *widget, gpointer data); +void cb_main_pubhubfilter_activate(GtkWidget *widget, gpointer data); +void cb_main_dcnctbtn_clicked(GtkWidget *widget, gpointer data); +void cb_main_phublist_cchange(GtkWidget *widget, gpointer data); +void cb_main_phublist_activate(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data); +void cb_main_chatnodes_activate(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data); +void cb_main_srchres_activate(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data); +void cb_main_chatstr_activate(GtkWidget *widget, gpointer data); +void cb_main_simplesrch_changed(GtkWidget *widget, gpointer data); +void cb_main_realsrch_changed(GtkWidget *widget, gpointer data); +void cb_main_srchbtn_clicked(GtkWidget *widget, gpointer data); +void cb_main_srchcanbtn_clicked(GtkWidget *widget, gpointer data); +void cb_main_trlist_keypress(GtkWidget *widget, GdkEventKey *event, gpointer data); +void cb_main_filternoslots_toggled(GtkToggleButton *widget, gpointer data); +void dcfdcallback(gpointer data, gint source, GdkInputCondition condition); +void srchstatupdate(void); +void transnicebytefunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); +void transerrorinfo(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); +void percentagefunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); +void hidezerofunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); +void speedtimefunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data); + +#define DCCHARSET "windows-1252" + +#define _(text) gettext(text) + +#include "mainwnd.gtk" +#include "inpdialog.gtk" +#include "pref.gtk" + +void updatewrite(void) +{ + if(dcfd < 0) + return; + if(dc_wantwrite()) + { + if(gdkwrite == -1) + gdkwrite = gdk_input_add(dcfd, GDK_INPUT_WRITE, dcfdcallback, NULL); + } else { + if(gdkwrite != -1) + { + gdk_input_remove(gdkwrite); + gdkwrite = -1; + } + } +} + +void fndestroycb(struct dc_fnetnode *fn) +{ + struct fndata *data; + GtkTextBuffer *textbuf; + + data = fn->udata; + g_object_unref(data->textbuf); + free(data); + if(curchat == fn->id) + { + textbuf = gtk_text_buffer_new(chattags); + gtk_text_view_set_buffer(GTK_TEXT_VIEW(main_chatview), textbuf); + g_object_unref(textbuf); + } +} + +void addfndata(struct dc_fnetnode *fn) +{ + struct fndata *data; + + if(fn->udata != NULL) + return; + fn->destroycb = fndestroycb; + data = smalloc(sizeof(*data)); + data->textbuf = gtk_text_buffer_new(chattags); + fn->udata = data; +} + +char *getfnstatestock(int state) +{ + if(state == DC_FNN_STATE_SYN) + return("gtk-jump-to"); + if(state == DC_FNN_STATE_HS) + return("gtk-execute"); + if(state == DC_FNN_STATE_EST) + return("gtk-yes"); + if(state == DC_FNN_STATE_DEAD) + return("gtk-cancel"); + return(NULL); +} + +void updatehublist(void) +{ + int done; + struct dc_fnetnode *fn; + GtkTreeIter iter; + int id; + char *buf; + char *name; + int state, numusers; + + for(fn = dc_fnetnodes; fn != NULL; fn = fn->next) + fn->found = 0; + done = 0; + while(!done) + { + done = 1; + if(gtk_tree_model_get_iter_first(GTK_TREE_MODEL(fnmodel), &iter)) + { + do + { + gtk_tree_model_get(GTK_TREE_MODEL(fnmodel), &iter, 0, &id, -1); + if((fn = dc_findfnetnode(id)) == NULL) + { + /* I can't seem to get a sensible reply fromp + * gtk_list_store, so I'm just doing this + * instead. */ + gtk_list_store_remove(fnmodel, &iter); + done = 0; + break; + } else { + gtk_tree_model_get(GTK_TREE_MODEL(fnmodel), &iter, 1, &name, 2, &state, 3, &numusers, -1); + if(fn->name == NULL) + buf = _("Unknown"); + else + buf = icswcstombs(fn->name, "UTF-8", NULL); + if(strcmp(buf, name)) + gtk_list_store_set(fnmodel, &iter, 1, buf, -1); + if(state != fn->state) + { + gtk_list_store_set(fnmodel, &iter, 2, fn->state, -1); + gtk_list_store_set(fnmodel, &iter, 4, getfnstatestock(fn->state), -1); + } + if(numusers != fn->numusers) + gtk_list_store_set(fnmodel, &iter, 3, fn->numusers, -1); + g_free(name); + fn->found = 1; + } + } while(gtk_tree_model_iter_next(GTK_TREE_MODEL(fnmodel), &iter)); + } + } + for(fn = dc_fnetnodes; fn != NULL; fn = fn->next) + { + if(!fn->found) + { + if(fn->name == NULL) + buf = _("Unknown"); + else + buf = icswcstombs(fn->name, "UTF-8", NULL); + gtk_list_store_append(fnmodel, &iter); + gtk_list_store_set(fnmodel, &iter, 0, fn->id, 1, buf, 2, fn->state, 3, fn->numusers, 4, getfnstatestock(fn->state), -1); + addfndata(fn); + } + } +} + +void percentagefunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) +{ + int colnum; + float val; + char buf[64]; + + colnum = (int)data; + gtk_tree_model_get(model, iter, colnum, &val, -1); + snprintf(buf, 64, "%.2f%%", (double)(val * 100.0)); + g_object_set(rend, "text", buf, NULL); +} + +void transnicebytefunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) +{ + int colnum, val; + char buf[64]; + + colnum = (int)data; + gtk_tree_model_get(model, iter, colnum, &val, -1); + if(val >= 0) + snprintf(buf, 64, "%'i", val); + else + strcpy(buf, _("Unknown")); + g_object_set(rend, "text", buf, NULL); +} + +void hidezerofunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) +{ + int colnum, val; + char buf[64]; + + colnum = (int)data; + gtk_tree_model_get(model, iter, colnum, &val, -1); + if(val > 0) + snprintf(buf, 64, "%i", val); + else + strcpy(buf, ""); + g_object_set(rend, "text", buf, NULL); +} + +void speedtimefunc(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) +{ + int speed, size, time; + char buf[64]; + + gtk_tree_model_get(model, iter, 4, &size, 8, &speed, -1); + if(speed > 0) + { + time = (size / speed) / 60; + if(time < 1) + snprintf(buf, 64, "%'i (<00:01)", speed); + else + snprintf(buf, 64, "%'i (%02i:%02i)", speed, time / 60, time % 60); + } else if(speed == 0) { + strcpy(buf, "0"); + } else { + strcpy(buf, _("Unknown")); + } + g_object_set(rend, "text", buf, NULL); +} + +void transerrorinfo(GtkTreeViewColumn *col, GtkCellRenderer *rend, GtkTreeModel *model, GtkTreeIter *iter, gpointer data) +{ + int error; + time_t errortime; + char finbuf[64], tbuf[64], *errstr; + + gtk_tree_model_get(model, iter, 10, &error, 11, &errortime, -1); + if(error != DC_TRNSE_NOERROR) + { + if(error == DC_TRNSE_NOTFOUND) + errstr = _("Not found"); + else if(error == DC_TRNSE_NOSLOTS) + errstr = _("No slots"); + strftime(tbuf, 64, _("%H:%M:%S"), localtime(&errortime)); + snprintf(finbuf, 64, _("%s (reported at %s)"), errstr, tbuf); + } else { + *finbuf = 0; + } + g_object_set(rend, "text", finbuf, NULL); +} + +char *gettrstatestock(int state) +{ + if(state == DC_TRNS_WAITING) + return("gtk-jump-to"); + if(state == DC_TRNS_HS) + return("gtk-execute"); + if(state == DC_TRNS_MAIN) + return("gtk-network"); + if(state == DC_TRNS_DONE) + return("gtk-yes"); + return(NULL); +} + +void updatetransferlists(void) +{ + int i; + int done; + struct dc_transfer *transfer; + GtkTreeIter iter; + int id; + char *buf; + char *peerid, *peernick, *path; + int state, dir, size, curpos, error; + time_t errortime; + GtkListStore *stores[3]; + + for(transfer = dc_transfers; transfer != NULL; transfer = transfer->next) + transfer->found = 0; + stores[DC_TRNSD_UNKNOWN] = NULL; + stores[DC_TRNSD_UP] = ulmodel; + stores[DC_TRNSD_DOWN] = dlmodel; + for(i = 0; i < 3; i++) + { + if(stores[i] == NULL) + continue; + done = 0; + while(!done) + { + done = 1; + if(gtk_tree_model_get_iter_first(GTK_TREE_MODEL(stores[i]), &iter)) + { + do + { + gtk_tree_model_get(GTK_TREE_MODEL(stores[i]), &iter, 0, &id, 1, &dir, -1); + if(((transfer = dc_findtransfer(id)) == NULL) || (transfer->dir != dir)) + { + gtk_list_store_remove(stores[i], &iter); + done = 0; + break; + } else { + transfer->found = 1; + gtk_tree_model_get(GTK_TREE_MODEL(stores[i]), &iter, 2, &state, 3, &peerid, 4, &peernick, 5, &path, 6, &size, 7, &curpos, 10, &error, 11, &errortime, -1); + if(state != transfer->state) + gtk_list_store_set(stores[i], &iter, 2, transfer->state, 8, gettrstatestock(transfer->state), -1); + if(size != transfer->size) + gtk_list_store_set(stores[i], &iter, 6, transfer->size, -1); + if(curpos != transfer->curpos) + gtk_list_store_set(stores[i], &iter, 7, transfer->curpos, -1); + if(error != transfer->error) + gtk_list_store_set(stores[i], &iter, 10, transfer->error, -1); + if(errortime != transfer->errortime) + gtk_list_store_set(stores[i], &iter, 11, transfer->errortime, -1); + if((transfer->size > 0) && (transfer->curpos > 0)) + gtk_list_store_set(stores[i], &iter, 9, (float)transfer->curpos / (float)transfer->size, -1); + buf = icswcstombs(transfer->peerid, "UTF-8", NULL); + if(strcmp(buf, peerid)) + gtk_list_store_set(stores[i], &iter, 3, buf, -1); + buf = icswcstombs(((transfer->peernick == NULL) || (transfer->peernick[0] == L'\0'))?transfer->peerid:transfer->peernick, "UTF-8", NULL); + if(strcmp(buf, peernick)) + gtk_list_store_set(stores[i], &iter, 4, buf, -1); + buf = (transfer->path == NULL)?_("Unknown"):icswcstombs(transfer->path, "UTF-8", NULL); + if(strcmp(buf, path)) + gtk_list_store_set(stores[i], &iter, 5, buf, -1); + g_free(peerid); + g_free(peernick); + g_free(path); + } + } while(gtk_tree_model_iter_next(GTK_TREE_MODEL(stores[i]), &iter)); + } + } + } + for(transfer = dc_transfers; transfer != NULL; transfer = transfer->next) + { + if(!transfer->found) + { + if(stores[transfer->dir] != NULL) + { + peerid = icwcstombs(transfer->peerid, "UTF-8"); + peernick = icwcstombs(((transfer->peernick == NULL) || (transfer->peernick[0] == L'\0'))?transfer->peerid:transfer->peernick, "UTF-8"); + path = (transfer->path == NULL)?_("Unknown"):icwcstombs(transfer->path, "UTF-8"); + gtk_list_store_append(stores[transfer->dir], &iter); + gtk_list_store_set(stores[transfer->dir], &iter, + 0, transfer->id, + 1, transfer->dir, + 2, transfer->state, + 3, peerid, + 4, peernick, + 5, path, + 6, transfer->size, + 7, transfer->curpos, + 8, gettrstatestock(transfer->state), + 9, 0.0, + 10, transfer->error, + 11, transfer->errortime, + -1); + free(peerid); + free(peernick); + if(transfer->path != NULL) + free(path); + } + } + } +} + +void updatesbar(char *msg) +{ + gtk_statusbar_pop(GTK_STATUSBAR(main_statusbar), 0); + gtk_statusbar_push(GTK_STATUSBAR(main_statusbar), 0, msg); +} + +void freesrchsizes(void) +{ + int i; + + for(i = 0; i < numsizes; i++) + { + if(srchsizes[i].ref != NULL) + gtk_tree_row_reference_free(srchsizes[i].ref); + } + if(srchsizes != NULL) + free(srchsizes); + srchsizes = NULL; + numsizes = 0; +} + +void dcdisconnected(void) +{ + if(gdkread != -1) + { + gdk_input_remove(gdkread); + gdkread = -1; + } + dcfd = -1; + updatehublist(); + updatetransferlists(); + cursrch = nextsrch = -1; + gtk_tree_store_clear(srchmodel); + freesrchsizes(); + gtk_widget_set_sensitive(main_connmenu, TRUE); + gtk_widget_set_sensitive(main_dconnmenu, FALSE); + gtk_widget_set_sensitive(main_simplesrch, TRUE); + gtk_widget_set_sensitive(main_realsrch, TRUE); + gtk_widget_set_sensitive(main_srchbtn, TRUE); + gtk_widget_set_sensitive(main_srchcanbtn, FALSE); + updatesbar(_("Disconnected")); +} + +char *inputbox(char *title, char *prompt, char *def, int echo) +{ + int resp; + GtkWidget *swnd; + char *buf; + + inpdialog = gtk_dialog_new_with_buttons(title, GTK_WINDOW(main_wnd), GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, NULL); + gtk_box_pack_start(GTK_BOX(GTK_DIALOG(inpdialog)->vbox), swnd = create_inpdialog_wnd(), TRUE, TRUE, 0); + gtk_widget_show(swnd); + if(!echo) + gtk_entry_set_visibility(GTK_ENTRY(inpdialog_entry), FALSE); + gtk_label_set_text(GTK_LABEL(inpdialog_prompt), prompt); + gtk_entry_set_text(GTK_ENTRY(inpdialog_entry), def); + resp = gtk_dialog_run(GTK_DIALOG(inpdialog)); + if(!echo) + gtk_entry_set_visibility(GTK_ENTRY(inpdialog_entry), TRUE); + if(resp == GTK_RESPONSE_ACCEPT) + buf = strdup(gtk_entry_get_text(GTK_ENTRY(inpdialog_entry))); + else + buf = NULL; + gtk_widget_destroy(inpdialog); + updatewrite(); + return(buf); +} + +int msgbox(int type, int buttons, char *format, ...) +{ + GtkWidget *swnd; + va_list args; + char *buf; + int resp; + + va_start(args, format); + buf = vsprintf2(format, args); + va_end(args); + swnd = gtk_message_dialog_new(GTK_WINDOW(main_wnd), GTK_DIALOG_MODAL, type, buttons, "%s", buf); + resp = gtk_dialog_run(GTK_DIALOG(swnd)); + gtk_widget_destroy(swnd); + free(buf); + return(resp); +} + +void readconfigfile(void) +{ + FILE *cfgfile; + char *homedir, *buf, *p; + int w, h; + + if((homedir = getenv("HOME")) == NULL) + { + fprintf(stderr, "warning: could not find home directory!\n"); + return; + } + buf = sprintf2("%s/.dolconrc", homedir); + if((cfgfile = fopen(buf, "r")) == NULL) + { + if(errno != ENOENT) + perror(buf); + free(buf); + return; + } + free(buf); + buf = smalloc(1024); + while(fgets(buf, 1024, cfgfile) != NULL) + { + if(strlen(buf) < 1) + continue; + p = buf + strlen(buf); + if(p[-1] == '\n') + *(--p) = 0; + if((p = strchr(buf, ':')) == NULL) + continue; + *(p++) = 0; + while((*p == ' ') || (*p == '\t')) + p++; + if(!strcmp(buf, "wnd-width")) + { + w = atoi(p); + } else if(!strcmp(buf, "wnd-height")) { + h = atoi(p); + } else if(!strcmp(buf, "pane1-pos")) { + gtk_paned_set_position(GTK_PANED(main_pane1), atoi(p)); + } else if(!strcmp(buf, "pane2-pos")) { + gtk_paned_set_position(GTK_PANED(main_pane2), atoi(p)); + } else if(!strcmp(buf, "pane3-pos")) { + gtk_paned_set_position(GTK_PANED(main_pane3), atoi(p)); + } else if(!strcmp(buf, "pubhubaddr")) { + free(pubhubaddr); + pubhubaddr = sstrdup(p); + } else if(!strcmp(buf, "dcserver")) { + free(dcserver); + dcserver = sstrdup(p); + } else if(!strcmp(buf, "advexpanded")) { + gtk_expander_set_expanded(GTK_EXPANDER(main_advexp), atoi(p)); + } else if(!strcmp(buf, "connectas")) { + free(connectas); + connectas = sstrdup(p); + } else if(!strcmp(buf, "autoconn")) { + autoconn = atoi(p); + } else if(!strcmp(buf, "filternoslots")) { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(main_filternoslots), atoi(p)); + } + } + free(buf); + fclose(cfgfile); +/* + if(w != 1589) + abort(); +*/ + gtk_window_resize(GTK_WINDOW(main_wnd), w, h); +} + +void updateconfigfile(void) +{ + FILE *cfgfile; + char *homedir, *buf; + int w, h; + + if((homedir = getenv("HOME")) == NULL) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Could not get your home directory!")); + return; + } + buf = sprintf2("%s/.dolconrc", homedir); + if((cfgfile = fopen(buf, "w")) == NULL) + { + free(buf); + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Could not open configuration file for writing: %s"), strerror(errno)); + return; + } + free(buf); + gtk_window_get_size(GTK_WINDOW(main_wnd), &w, &h); + fprintf(cfgfile, "wnd-width: %i\n", w); + fprintf(cfgfile, "wnd-height: %i\n", h); + fprintf(cfgfile, "pane1-pos: %i\n", gtk_paned_get_position(GTK_PANED(main_pane1))); + fprintf(cfgfile, "pane2-pos: %i\n", gtk_paned_get_position(GTK_PANED(main_pane2))); + fprintf(cfgfile, "pane3-pos: %i\n", gtk_paned_get_position(GTK_PANED(main_pane3))); + fprintf(cfgfile, "pubhubaddr: %s\n", pubhubaddr); + fprintf(cfgfile, "dcserver: %s\n", dcserver); + fprintf(cfgfile, "advexpanded: %i\n", gtk_expander_get_expanded(GTK_EXPANDER(main_advexp))); + fprintf(cfgfile, "connectas: %s\n", connectas); + fprintf(cfgfile, "autoconn: %i\n", autoconn); + fprintf(cfgfile, "filternoslots: %i\n", gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(main_filternoslots))); + fclose(cfgfile); +} + +gboolean initdeath(GtkWidget *widget, gpointer data) +{ + updateconfigfile(); + gtk_main_quit(); + return(TRUE); +} + +void cb_inpdialog_entry_activate(GtkWidget *widget, gpointer data) +{ + gtk_dialog_response(GTK_DIALOG(inpdialog), GTK_RESPONSE_ACCEPT); +} + +int loginconv(int type, wchar_t *prompt, char **resp, void *data) +{ + int ret; + char *buf; + + ret = 0; + buf = icwcstombs(prompt, "UTF-8"); + switch(type) + { + case DC_LOGIN_CONV_NOECHO: + if((*resp = inputbox(_("Login"), buf, "", 0)) == NULL) + ret = 1; + break; + case DC_LOGIN_CONV_ECHO: + if((*resp = inputbox(_("Login"), buf, "", 1)) == NULL) + ret = 1; + break; + case DC_LOGIN_CONV_INFO: + msgbox(GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "%s", buf); + break; + case DC_LOGIN_CONV_ERROR: + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", buf); + break; + } + free(buf); + updatewrite(); + return(ret); +} + +void getfnlistcallback(int resp, void *data) +{ + updatehublist(); +} + +void gettrlistcallback(int resp, void *data) +{ + updatetransferlists(); +} + +void logincallback(int err, wchar_t *reason, void *data) +{ + switch(err) + { + case DC_LOGIN_ERR_SUCCESS: + dc_queuecmd(NULL, NULL, L"notify", L"all", L"on", NULL); + dc_getfnlistasync(getfnlistcallback, NULL); + dc_gettrlistasync(gettrlistcallback, NULL); + updatesbar("Authenticated"); + break; + case DC_LOGIN_ERR_NOLOGIN: + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Could not negotiate an acceptable authentication mechanism")); + dc_disconnect(); + dcdisconnected(); + break; + case DC_LOGIN_ERR_SERVER: + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("The server has encountered an error")); + dc_disconnect(); + dcdisconnected(); + break; + case DC_LOGIN_ERR_USER: + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Internal client error")); + dc_disconnect(); + dcdisconnected(); + break; + case DC_LOGIN_ERR_CONV: + dc_disconnect(); + dcdisconnected(); + break; + case DC_LOGIN_ERR_AUTHFAIL: + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Login attempt failed!")); + dc_disconnect(); + dcdisconnected(); + break; + } + updatewrite(); +} + +GtkTreeIter *ref2iter(GtkTreeRowReference *ref) +{ + static GtkTreeIter iter; + GtkTreePath *path; + + assert((path = gtk_tree_row_reference_get_path(ref)) != NULL); + assert(gtk_tree_model_get_iter(GTK_TREE_MODEL(srchmodel), &iter, path)); + gtk_tree_path_free(path); + return(&iter); +} + +GtkTreeRowReference *iter2ref(GtkTreeIter *iter) +{ + GtkTreePath *path; + GtkTreeRowReference *ref; + + assert((path = gtk_tree_model_get_path(GTK_TREE_MODEL(srchmodel), iter)) != NULL); + assert((ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(srchmodel), path)) != NULL); + gtk_tree_path_free(path); + return(ref); +} + +struct srchsize *finddiscsize(void) +{ + int i; + GtkTreeIter iter; + + for(i = 0; i < numsizes; i++) + { + if(srchsizes[i].size == -1) + return(&srchsizes[i]); + } + srchsizes = srealloc(srchsizes, sizeof(*srchsizes) * ++numsizes); + srchsizes[i].size = -1; + srchsizes[i].num = 1; + srchsizes[i].slots = 0; + srchsizes[i].resptime = 0.0; + gtk_tree_store_append(srchmodel, &iter, NULL); + gtk_tree_store_set(srchmodel, &iter, 3, _("Discrete sizes"), 7, 1, -1); + srchsizes[i].ref = iter2ref(&iter); + return(&srchsizes[i]); +} + +struct knownspeed *findksentbyname(char *userid) +{ + int i; + + for(i = 0; i < numspeeds; i++) + { + if(!strcmp(knownspeeds[i].userid, userid)) + return(&knownspeeds[i]); + } + return(NULL); +} + +struct knownspeed *findksentbyseq(int seq) +{ + int i; + + for(i = 0; i < numspeeds; i++) + { + if(knownspeeds[i].seq == seq) + return(&knownspeeds[i]); + } + return(NULL); +} + +gboolean ksupdaterow(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data) +{ + struct knownspeed *ks; + char *userid; + + gtk_tree_model_get(GTK_TREE_MODEL(model), iter, 1, &userid, -1); + if(userid == NULL) + return(FALSE); + ks = findksentbyname(userid); + if(ks == NULL) + { + knownspeeds = srealloc(knownspeeds, (numspeeds + 1) * sizeof(*knownspeeds)); + ks = &knownspeeds[numspeeds]; + numspeeds++; + ks->userid = sstrdup(userid); + ks->speed = -1; + ks->seq = -2; + ksqueryseq = -2; + } + g_free(userid); + if(ks->speed != -1) + gtk_tree_store_set(GTK_TREE_STORE(model), iter, 8, ks->speed, -1); + return(FALSE); +} + +gint ksupdatecb(gpointer data) +{ + int i, oldnum; + time_t now; + wchar_t **users, *buf; + size_t userssize, usersdata; + + if(ksquerytag != -1) + return(TRUE); + now = time(NULL); + oldnum = numspeeds; + for(i = 0; i < numspeeds;) + { + if(now - knownspeeds[i].fetched > 60) + { + free(knownspeeds[i].userid); + memmove(&knownspeeds[i], &knownspeeds[i + 1], (--numspeeds - i) * sizeof(*knownspeeds)); + } else { + i++; + } + } + if(oldnum != numspeeds) + knownspeeds = srealloc(knownspeeds, numspeeds * sizeof(*knownspeeds)); + gtk_tree_model_foreach(GTK_TREE_MODEL(srchmodel), ksupdaterow, NULL); + if(ksqueryseq == -2) + { + users = NULL; + userssize = usersdata = 0; + ksqueryseq = 0; + for(i = 0; i < numspeeds; i++) + { + if(knownspeeds[i].seq == -2) + { + assert((buf = icmbstowcs(knownspeeds[i].userid, "UTF-8")) != NULL); + knownspeeds[i].seq = ksqueryseq++; + addtobuf(users, buf); + } + } + addtobuf(users, NULL); + ksquerytag = dc_queuecmd(NULL, NULL, L"filtercmd", L"userspeeda", L"%%a", users, NULL); + dc_freewcsarr(users); + } + return(TRUE); +} + +void handleresps(void) +{ + int i; + struct dc_response *resp; + struct dc_intresp *ires; + struct dc_fnetnode *fn; + struct fndata *fndata; + GtkTextIter iter; + GtkTreeIter titer, piter; + char *buf, *p; + int tosbuf; + struct srchsize *ss; + struct knownspeed *ks; + + while((resp = dc_getresp()) != NULL) + { + if(!wcscmp(resp->cmdname, L".connect")) + { + if(resp->code == 200) + { + tosbuf = 0x10; /* Minimum cost */ + setsockopt(dcfd, SOL_IP, IP_TOS, &tosbuf, sizeof(tosbuf)); + updatesbar(_("Connected")); + dc_loginasync(connectas, 1, loginconv, logincallback, NULL); + } else { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("The server refused the connection")); + dc_disconnect(); + dcdisconnected(); + } + } else if(!wcscmp(resp->cmdname, L".notify")) { + dc_uimisc_handlenotify(resp); + switch(resp->code) + { + case 600: + if((ires = dc_interpret(resp)) != NULL) + { + if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL) + { + fndata = fn->udata; + gtk_text_buffer_get_end_iter(fndata->textbuf, &iter); + if((buf = icwcstombs(ires->argv[3].val.str, "UTF-8")) != NULL) + { + gtk_text_buffer_insert_with_tags_by_name(fndata->textbuf, &iter, "<", -1, "sender", NULL); + gtk_text_buffer_insert_with_tags_by_name(fndata->textbuf, &iter, buf, -1, "sender", NULL); + gtk_text_buffer_insert_with_tags_by_name(fndata->textbuf, &iter, ">", -1, "sender", NULL); + gtk_text_buffer_insert(fndata->textbuf, &iter, " ", -1); + free(buf); + } + if((buf = icwcstombs(ires->argv[4].val.str, "UTF-8")) != NULL) + { + gtk_text_buffer_insert(fndata->textbuf, &iter, buf, -1); + gtk_text_buffer_insert(fndata->textbuf, &iter, "\n", -1); + free(buf); + if(curchat == fn->id) + gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(main_chatview), &iter, 0, 0, 0, 0); + } + } + dc_freeires(ires); + } + break; + case 601: + case 602: + case 603: + case 604: + case 605: + updatehublist(); + break; + case 610: + case 611: + case 612: + case 613: + case 614: + case 615: + case 616: + case 617: + updatetransferlists(); + break; + case 620: + if((ires = dc_interpret(resp)) != NULL) + { + if(ires->argv[0].val.num == nextsrch) + srcheta = time(NULL) + ires->argv[0].val.num; + dc_freeires(ires); + } + break; + case 621: + if((ires = dc_interpret(resp)) != NULL) + { + if(ires->argv[0].val.num == nextsrch) + { + if(cursrch != -1) + dc_queuecmd(NULL, NULL, L"cansrch", L"%%i", cursrch, NULL); + cursrch = nextsrch; + nextsrch = -1; + gtk_widget_set_sensitive(main_realsrch, TRUE); + gtk_widget_set_sensitive(main_simplesrch, TRUE); + gtk_widget_set_sensitive(main_srchbtn, TRUE); + gtk_widget_set_sensitive(main_srchcanbtn, FALSE); + srchstatupdate(); + gtk_entry_set_text(GTK_ENTRY(main_realsrch), ""); + gtk_entry_set_text(GTK_ENTRY(main_simplesrch), ""); + gtk_tree_store_clear(srchmodel); + freesrchsizes(); + } + dc_freeires(ires); + } + break; + case 622: + if((ires = dc_interpret(resp)) != NULL) + { + if(ires->argv[0].val.num == cursrch) + { + for(i = 0; i < numsizes; i++) + { + if(srchsizes[i].size == ires->argv[4].val.num) + break; + } + if(i == numsizes) + { + srchsizes = srealloc(srchsizes, sizeof(*srchsizes) * ++numsizes); + srchsizes[i].size = ires->argv[4].val.num; + srchsizes[i].num = 1; + srchsizes[i].slots = ires->argv[5].val.num; + srchsizes[i].resptime = ires->argv[7].val.flnum; + ss = finddiscsize(); + ss->slots += ires->argv[5].val.num; + if((ss->resptime == 0.0) || (ss->resptime > ires->argv[7].val.flnum)) + ss->resptime = ires->argv[7].val.flnum; + piter = *ref2iter(ss->ref); + gtk_tree_store_set(srchmodel, &piter, 5, ss->slots, 6, ss->resptime, -1); + gtk_tree_store_append(srchmodel, &titer, &piter); + srchsizes[i].ref = iter2ref(&titer); + } else if(srchsizes[i].num == 1) { + char *filename, *peername, *fnetname; + int slots, speed; + double resptime; + + gtk_tree_model_get(GTK_TREE_MODEL(srchmodel), ref2iter(srchsizes[i].ref), 0, &fnetname, 1, &peername, 3, &filename, 5, &slots, 6, &resptime, 8, &speed, -1); + gtk_tree_store_remove(srchmodel, ref2iter(srchsizes[i].ref)); + gtk_tree_row_reference_free(srchsizes[i].ref); + ss = finddiscsize(); + ss->slots -= slots; + gtk_tree_store_set(srchmodel, ref2iter(ss->ref), 5, ss->slots, -1); + gtk_tree_store_append(srchmodel, &piter, NULL); + srchsizes[i].slots = ires->argv[5].val.num + slots; + srchsizes[i].resptime = (ires->argv[7].val.flnum < resptime)?ires->argv[7].val.flnum:resptime; + srchsizes[i].num = 2; + srchsizes[i].ref = iter2ref(&piter); + gtk_tree_store_set(srchmodel, &piter, 4, srchsizes[i].size, 5, srchsizes[i].slots, 6, srchsizes[i].resptime, 7, 2, -1); + if((buf = icwcstombs(ires->argv[1].val.str, "UTF-8")) != NULL) + { + p = buf; + /* XXX: Too DC-specific! */ + if(strrchr(p, '\\') != NULL) + p = strrchr(p, '\\') + 1; + gtk_tree_store_set(srchmodel, &piter, 3, p, -1); + free(buf); + } + gtk_tree_store_append(srchmodel, &titer, &piter); + gtk_tree_store_set(srchmodel, &titer, 0, fnetname, 1, peername, 2, peername, 3, filename, 4, srchsizes[i].size, 5, slots, 6, resptime, 8, speed, -1); + g_free(filename); g_free(peername); g_free(fnetname); + gtk_tree_store_append(srchmodel, &titer, &piter); + } else { + srchsizes[i].num++; + srchsizes[i].slots += ires->argv[5].val.num; + if(ires->argv[7].val.flnum < srchsizes[i].resptime) + srchsizes[i].resptime = ires->argv[7].val.flnum; + piter = *ref2iter(srchsizes[i].ref); + gtk_tree_store_set(srchmodel, &piter, 5, srchsizes[i].slots, 6, srchsizes[i].resptime, 7, srchsizes[i].num, -1); + gtk_tree_store_append(srchmodel, &titer, &piter); + } + if((buf = icwcstombs(ires->argv[1].val.str, "UTF-8")) != NULL) + { + gtk_tree_store_set(srchmodel, &titer, 3, buf, -1); + free(buf); + } + if((buf = icwcstombs(ires->argv[2].val.str, "UTF-8")) != NULL) + { + gtk_tree_store_set(srchmodel, &titer, 0, buf, -1); + free(buf); + } + if((buf = icwcstombs(ires->argv[3].val.str, "UTF-8")) != NULL) + { + gtk_tree_store_set(srchmodel, &titer, 1, buf, -1); + gtk_tree_store_set(srchmodel, &titer, 2, buf, -1); + free(buf); + } + gtk_tree_store_set(srchmodel, &titer, 4, ires->argv[4].val.num, 5, ires->argv[5].val.num, 6, ires->argv[7].val.flnum, 8, -1, -1); + } + dc_freeires(ires); + } + break; + default: + break; + } + } else if(!wcscmp(resp->cmdname, L"filtercmd")) { + if((ksquerytag >= 0) && (ksquerytag == resp->tag)) + { + for(i = 0; i < resp->numlines; i++) + { + assert((ks = findksentbyseq(i)) != NULL); + ks->speed = wcstol(resp->rlines[i].argv[1], NULL, 10); + ks->seq = -1; + ks->fetched = time(NULL); + } + ksquerytag = -1; + ksupdatecb(NULL); + } + } + dc_freeresp(resp); + } + updatewrite(); +} + +void dcfdcallback(gpointer data, gint source, GdkInputCondition condition) +{ + int errnobak; + + if(((condition & GDK_INPUT_READ) && dc_handleread()) || ((condition & GDK_INPUT_WRITE) && dc_handlewrite())) + { + errnobak = errno; + dcdisconnected(); + if(errnobak == 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("The server has closed the connection")); + } else { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("The connection to the server failed:\n\n%s"), strerror(errnobak)); + } + return; + } + handleresps(); +} + +void cb_main_dconnmenu_activate(GtkWidget *widget, gpointer data) +{ + if(dcfd < 0) + return; + dc_disconnect(); + dcdisconnected(); +} + +void cb_main_prefmenu_activate(GtkWidget *widget, gpointer data) +{ + GtkWidget *dialog, *swnd; + int resp; + + dialog = gtk_dialog_new_with_buttons(_("Preferences"), GTK_WINDOW(main_wnd), GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, NULL); + gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dialog)->vbox), swnd = create_pref_wnd(), TRUE, TRUE, 0); + gtk_entry_set_text(GTK_ENTRY(pref_pubhuburl), pubhubaddr); + gtk_entry_set_text(GTK_ENTRY(pref_connectas), connectas); + gtk_entry_set_text(GTK_ENTRY(pref_dcserver), dcserver); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pref_autoconn), autoconn); + gtk_widget_show(swnd); + resp = gtk_dialog_run(GTK_DIALOG(dialog)); + if(resp == GTK_RESPONSE_ACCEPT) + { + free(pubhubaddr); + pubhubaddr = sstrdup(gtk_entry_get_text(GTK_ENTRY(pref_pubhuburl))); + free(connectas); + connectas = sstrdup(gtk_entry_get_text(GTK_ENTRY(pref_connectas))); + free(dcserver); + dcserver = sstrdup(gtk_entry_get_text(GTK_ENTRY(pref_dcserver))); + autoconn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pref_autoconn)); + } + gtk_widget_destroy(dialog); +} + +void dcconnect(char *host) +{ + dcfd = dc_connect(host, -1); + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Could not connect:\n\n%s"), strerror(errno)); + return; + } + gdkread = gdk_input_add(dcfd, GDK_INPUT_READ, dcfdcallback, NULL); + updatewrite(); + gtk_widget_set_sensitive(main_connmenu, FALSE); + gtk_widget_set_sensitive(main_dconnmenu, TRUE); + updatesbar(_("Connecting...")); +} + +void cb_main_connmenu_activate(GtkWidget *widget, gpointer data) +{ + char *buf; + + if(dcfd >= 0) + return; + if((buf = inputbox(_("Connect"), _("Server address:"), dcserver, 1)) == NULL) + return; + dcconnect(buf); + free(buf); +} + +void cb_main_sdmenu_activate(GtkWidget *widget, gpointer data) +{ + int tag; + struct dc_response *resp; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + tag = dc_queuecmd(NULL, NULL, L"shutdown", NULL); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + dc_freeresp(resp); + } + handleresps(); +} + +void cb_main_fnaddr_activate(GtkWidget *widget, gpointer data) +{ + int tag; + char *buf; + struct dc_response *resp; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + buf = sstrdup(gtk_entry_get_text(GTK_ENTRY(main_fnaddr))); + if(strchr(buf, ':') == NULL) + { + buf = srealloc(buf, strlen(buf) + 5); + strcat(buf, ":411"); + } + tag = dc_queuecmd(NULL, NULL, L"cnct", L"dc", L"%%s", buf, NULL); + free(buf); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + if(resp->code == 509) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("The server could not parse that address")); + dc_freeresp(resp); + } + gtk_entry_set_text(GTK_ENTRY(main_fnaddr), ""); + handleresps(); +} + +void pubhubfdcallback(gpointer data, gint source, GdkInputCondition condition) +{ + static char buf[65536]; + static int bufpos = 0; + int ret, i; + char *p, *p2; + char *fields[4]; + wchar_t *wbuf; + GtkTreeIter iter; + int sorted, sortcol; + GtkSortType sortorder; + GtkTreeModel *sortmodel; + + if(!(condition & GDK_INPUT_READ)) + return; + if(bufpos == 1024) + bufpos = 0; + ret = read(pubhubfd, buf + bufpos, sizeof(buf) - bufpos); + if(ret <= 0) + { + if(ret < 0) + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Could not read from public hub listing process: %s"), strerror(errno)); + close(pubhubfd); + gdk_input_remove(pubhubtag); + kill(pubhubproc, SIGINT); + pubhubfd = pubhubtag = -1; + pubhubproc = 0; + bufpos = 0; + if(filterpubhub) + { + regfree(&pubhubfilter); + filterpubhub = 0; + } + return; + } + bufpos += ret; + sortmodel = gtk_tree_view_get_model(GTK_TREE_VIEW(main_phublist)); + sorted = gtk_tree_sortable_get_sort_column_id(GTK_TREE_SORTABLE(sortmodel), &sortcol, &sortorder); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_phublist), NULL); + while((p = memchr(buf, '\n', bufpos)) != NULL) + { + *(p++) = 0; + if(!filterpubhub || !regexec(&pubhubfilter, buf, 0, NULL, 0)) + { + p2 = buf; + for(i = 0; i < 4; i++) + { + fields[i] = p2; + if((p2 = strchr(p2, '|')) == NULL) + break; + *(p2++) = 0; + } + if(i == 4) + { + for(i = 0; i < 4; i++) + { + if((wbuf = icsmbstowcs(fields[i], DCCHARSET, NULL)) == NULL) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Could not decode hublist - aborting at this point: %s"), strerror(errno)); + kill(pubhubproc, SIGINT); + break; + } + if((fields[i] = icwcstombs(wbuf, "UTF-8")) == NULL) + break; + } + if(i == 4) + { + gtk_list_store_append(pubhubmodel, &iter); + gtk_list_store_set(pubhubmodel, &iter, 0, fields[0], 1, fields[1], 2, fields[2], 3, atoi(fields[3]), -1); + } + for(i--; i >= 0; i--) + free(fields[i]); + } + } + memmove(buf, p, bufpos -= p - buf); + } + sortmodel = gtk_tree_model_sort_new_with_model(GTK_TREE_MODEL(pubhubmodel)); + if(sorted) + gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(sortmodel), sortcol, sortorder); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_phublist), sortmodel); +} + +void cb_main_pubhubfilter_activate(GtkWidget *widget, gpointer data) +{ + int pipe1[2], pipe2[2]; + int len, err; + const char *buf; + char errbuf[1024]; + + if(pubhubtag >= 0) + gdk_input_remove(pubhubtag); + if(pubhubfd >= 0) + close(pubhubfd); + if(pubhubproc > 0) + kill(pubhubproc, SIGINT); + if(filterpubhub) + { + regfree(&pubhubfilter); + filterpubhub = 0; + } + buf = gtk_entry_get_text(GTK_ENTRY(main_pubhubfilter)); + if(*buf) + { + if((err = regcomp(&pubhubfilter, buf, REG_EXTENDED | REG_ICASE | REG_NOSUB)) != 0) + { + regerror(err, &pubhubfilter, errbuf, sizeof(errbuf)); + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Could not compile regex: %s", errbuf); + regfree(&pubhubfilter); + filterpubhub = 0; + return; + } + filterpubhub = 1; + } + gtk_list_store_clear(pubhubmodel); + pipe(pipe1); + if((pubhubproc = fork()) == 0) + { + dup2(pipe1[1], 1); + close(pipe1[0]); + close(pipe1[1]); + execlp("wget", "wget", "-qO", "-", pubhubaddr, NULL); + perror("wget"); + exit(127); + } + close(pipe1[1]); + pubhubfd = pipe1[0]; + len = strlen(pubhubaddr); + if((len > 4) && !strcmp(pubhubaddr + len - 4, ".bz2")) + { + pipe(pipe2); + if(fork() == 0) + { + dup2(pipe1[0], 0); + dup2(pipe2[1], 1); + close(pipe1[0]); + close(pipe2[0]); + close(pipe2[1]); + execlp("bzcat", "bzcat", NULL); + perror("bzcat"); + exit(127); + } + close(pipe1[0]); + close(pipe2[1]); + pubhubfd = pipe2[0]; + } + pubhubtag = gdk_input_add(pubhubfd, GDK_INPUT_READ, pubhubfdcallback, NULL); +} + +void cb_main_dcnctbtn_clicked(GtkWidget *widget, gpointer data) +{ + GtkTreeIter iter; + int tag, id; + struct dc_response *resp; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + if(!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(main_fnetnodes)), NULL, &iter)) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("No hub selected")); + return; + } + gtk_tree_model_get(GTK_TREE_MODEL(fnmodel), &iter, 0, &id, -1); + tag = dc_queuecmd(NULL, NULL, L"dcnct", L"%%i", id, NULL); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + dc_freeresp(resp); + } + handleresps(); +} + +void cb_main_phublist_cchange(GtkWidget *widget, gpointer data) +{ + GtkTreeIter iter; + GtkTreeModel *model; + char *addr; + + if(!gtk_tree_selection_get_selected(gtk_tree_view_get_selection(GTK_TREE_VIEW(main_phublist)), &model, &iter)) + return; + gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, 1, &addr, -1); + gtk_entry_set_text(GTK_ENTRY(main_fnaddr), addr); + g_free(addr); +} + +void cb_main_phublist_activate(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) +{ + int tag; + struct dc_response *resp; + GtkTreeIter iter; + GtkTreeModel *model; + char *buf; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + model = gtk_tree_view_get_model(GTK_TREE_VIEW(widget)); + if(!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) + return; + gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, 1, &buf, -1); + if(strchr(buf, ':') == NULL) + { + buf = g_realloc(buf, strlen(buf) + 5); + strcat(buf, ":411"); + } + tag = dc_queuecmd(NULL, NULL, L"cnct", L"dc", L"%%s", buf, NULL); + g_free(buf); + gtk_entry_set_text(GTK_ENTRY(main_fnaddr), ""); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + if(resp->code == 509) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("The server could not parse that address")); + dc_freeresp(resp); + } + handleresps(); +} + +void cb_main_chatnodes_activate(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col, gpointer uudata) +{ + GtkTreeIter iter; + int id; + struct dc_fnetnode *fn; + struct fndata *data; + + if(!gtk_tree_model_get_iter(GTK_TREE_MODEL(fnmodel), &iter, path)) + return; + gtk_tree_model_get(GTK_TREE_MODEL(fnmodel), &iter, 0, &id, -1); + if((fn = dc_findfnetnode(id)) == NULL) + return; + data = fn->udata; + curchat = id; + if(gtk_tree_model_get_iter_first(GTK_TREE_MODEL(fnmodel), &iter)) + { + do + { + gtk_tree_model_get(GTK_TREE_MODEL(fnmodel), &iter, 0, &id, -1); + if(id == curchat) + gtk_list_store_set(fnmodel, &iter, 5, "gtk-apply", -1); + else + gtk_list_store_set(fnmodel, &iter, 5, NULL, -1); + } while(gtk_tree_model_iter_next(GTK_TREE_MODEL(fnmodel), &iter)); + } + gtk_text_view_set_buffer(GTK_TEXT_VIEW(main_chatview), GTK_TEXT_BUFFER(data->textbuf)); +} + +void cb_main_chatstr_activate(GtkWidget *widget, gpointer data) +{ + int tag; + const char *buf; + struct dc_response *resp; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + if(curchat < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("No hub selected")); + return; + } + buf = gtk_entry_get_text(GTK_ENTRY(main_chatstr)); + tag = dc_queuecmd(NULL, NULL, L"sendchat", L"%%i", curchat, L"1", L"", L"%%s", buf, NULL); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + else if(resp->code == 504) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("This hub could not support all the types of characters in your chat message")); + else if(resp->code == 513) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("This hub does not support chatting")); + else if(resp->code != 200) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("An error occurred while trying to chat (%i)"), resp->code); + dc_freeresp(resp); + } + gtk_entry_set_text(GTK_ENTRY(main_chatstr), ""); + handleresps(); +} + +void updatesrchfld(const char *simple) +{ + char *buf, *s; + char *p, *p2; + size_t bufsize, bufdata; + + s = sstrdup(simple); + buf = NULL; + bufsize = bufdata = 0; + p = s; + do + { + p2 = strchr(p, ' '); + if(p2 != NULL) + *(p2++) = 0; + if(*p) + { + if(bufdata > 0) + bufcat(buf, " & ", 3); + bufcat(buf, "N~", 2); + for(; *p; p++) + { + if(strchr("[]()$^.*?+\\|\"", *p) != NULL) + addtobuf(buf, '\\'); + addtobuf(buf, *p); + } + } + p = p2; + } while(p2 != NULL); + addtobuf(buf, 0); + gtk_entry_set_text(GTK_ENTRY(main_realsrch), buf); + free(buf); + free(s); +} + +void cb_main_simplesrch_changed(GtkWidget *widget, gpointer data) +{ + if(srchautoupdate) + return; + srchautoupdate = 1; + updatesrchfld(gtk_entry_get_text(GTK_ENTRY(main_simplesrch))); + srchautoupdate = 0; +} + +void cb_main_realsrch_changed(GtkWidget *widget, gpointer data) +{ + if(srchautoupdate) + return; + srchautoupdate = 1; + gtk_entry_set_text(GTK_ENTRY(main_simplesrch), ""); + srchautoupdate = 0; +} + +void cb_main_srchbtn_clicked(GtkWidget *widget, gpointer data) +{ + wchar_t **toks; + int tag; + struct dc_response *resp; + struct dc_intresp *ires; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + if(nextsrch != -1) /* Impossible case, but oh well... */ + return; + toks = dc_lexsexpr(icsmbstowcs((char *)gtk_entry_get_text(GTK_ENTRY(main_realsrch)), "UTF-8", NULL)); + if(*toks == NULL) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Please enter a search expression before searching")); + return; + } + tag = dc_queuecmd(NULL, NULL, L"search", L"all", L"%%a", toks, NULL); + dc_freewcsarr(toks); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 501) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("Could not find any hubs to search on")); + else if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + else if(resp->code == 509) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("The server could not parse your search expression")); + else if(resp->code != 200) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("An error occurred while trying to search (%i)"), resp->code); + if(resp->code == 200) + { + if((ires = dc_interpret(resp)) != NULL) + { + nextsrch = ires->argv[0].val.num; + srcheta = time(NULL) + ires->argv[1].val.num; + dc_freeires(ires); + } + gtk_widget_set_sensitive(main_realsrch, FALSE); + gtk_widget_set_sensitive(main_simplesrch, FALSE); + gtk_widget_set_sensitive(main_srchbtn, FALSE); + gtk_widget_set_sensitive(main_srchcanbtn, TRUE); + srchstatupdate(); + } + dc_freeresp(resp); + } + handleresps(); +} + +void cb_main_srchcanbtn_clicked(GtkWidget *widget, gpointer data) +{ + if(nextsrch == -1) + return; + dc_queuecmd(NULL, NULL, L"cansrch", L"%%i", nextsrch, NULL); + nextsrch = -1; + gtk_widget_set_sensitive(main_realsrch, TRUE); + gtk_widget_set_sensitive(main_simplesrch, TRUE); + gtk_widget_set_sensitive(main_srchbtn, TRUE); + gtk_widget_set_sensitive(main_srchcanbtn, FALSE); + srchstatupdate(); +} + +void cb_main_trlist_keypress(GtkWidget *widget, GdkEventKey *event, gpointer data) +{ + int id, tag; + GtkTreeSelection *sel; + GtkTreeModel *model; + GtkTreeIter iter; + struct dc_response *resp; + + if((event->type == GDK_KEY_PRESS) && (event->keyval == GDK_Delete)) + { + sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)); + if(gtk_tree_selection_get_selected(sel, &model, &iter)) + { + gtk_tree_model_get(model, &iter, 0, &id, -1); + tag = dc_queuecmd(NULL, NULL, L"cancel", L"%%i", id, NULL); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + else if(resp->code != 200) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("An error occurred while trying to cancel (%i)"), resp->code); + dc_freeresp(resp); + } + handleresps(); + } + } +} + +void cb_main_srchres_activate(GtkWidget *widget, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data) +{ + int tag; + struct dc_response *resp; + GtkTreeIter iter; + GtkTreeModel *model; + int size, num; + char *tfnet, *tpeerid, *tfilename, *arg; + wchar_t *fnet, *peerid, *filename; + + if(dcfd < 0) + { + msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, _("Not connected to DC server")); + return; + } + model = gtk_tree_view_get_model(GTK_TREE_VIEW(widget)); + if(!gtk_tree_model_get_iter(GTK_TREE_MODEL(model), &iter, path)) + return; + gtk_tree_model_get(model, &iter, 7, &num, -1); + if(num > 0) + return; + gtk_tree_model_get(GTK_TREE_MODEL(model), &iter, 0, &tfnet, 1, &tpeerid, 3, &tfilename, 4, &size, -1); + fnet = icmbstowcs(tfnet, "UTF-8"); + peerid = icmbstowcs(tpeerid, "UTF-8"); + filename = icmbstowcs(tfilename, "UTF-8"); + if((fnet == NULL) || (peerid == NULL) || (filename == NULL)) + { + if(fnet != NULL) + free(fnet); + if(peerid != NULL) + free(peerid); + if(filename != NULL) + free(filename); + g_free(tfnet); + g_free(tpeerid); + g_free(tfilename); + return; + } + g_free(tfnet); + g_free(tpeerid); + g_free(tfilename); + arg = (char *)gtk_entry_get_text(GTK_ENTRY(main_dlarg)); + if(*arg) + tag = dc_queuecmd(NULL, NULL, L"download", fnet, L"%%ls", peerid, L"%%ls", filename, L"%%i", size, L"user", L"%%s", arg, NULL); + else + tag = dc_queuecmd(NULL, NULL, L"download", fnet, L"%%ls", peerid, L"%%ls", filename, L"%%i", size, NULL); + free(fnet); + free(peerid); + free(filename); + if((resp = dc_gettaggedrespsync(tag)) != NULL) + { + if(resp->code == 502) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("You do not have permission to do that")); + if(resp->code != 200) + msgbox(GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, _("An error occurred while trying to queue the download (%i)"), resp->code); + dc_freeresp(resp); + } + handleresps(); +} + +gboolean srchfilterfunc(GtkTreeModel *model, GtkTreeIter *iter, gpointer data) +{ + int slots; + int filteratall; + + filteratall = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(main_filternoslots)); + if(!filteratall) + return(TRUE); + gtk_tree_model_get(model, iter, 5, &slots, -1); + if(slots < 1) + return(FALSE); + return(TRUE); +} + +void cb_main_filternoslots_toggled(GtkToggleButton *widget, gpointer data) +{ + gtk_tree_model_filter_refilter(srchmodelfilter); +} + +void srchstatupdate(void) +{ + char buf[1024]; + + if(nextsrch == -1) + { + snprintf(buf, 1024, _("Ready to search")); + } else { + snprintf(buf, 1024, _("Search scheduled and will be submitted in %i seconds"), (int)(srcheta - time(NULL))); + } + if(strcmp(gtk_label_get_text(GTK_LABEL(main_srchstatus)), buf)) + gtk_label_set_text(GTK_LABEL(main_srchstatus), buf); +} + +gint srchstatupdatecb(gpointer data) +{ + srchstatupdate(); + return(TRUE); +} + +void initchattags(void) +{ + GtkTextTag *tag; + + chattags = gtk_text_tag_table_new(); + tag = gtk_text_tag_new("sender"); + g_object_set(tag, "foreground", "blue", NULL); + gtk_text_tag_table_add(chattags, tag); +} + +int main(int argc, char **argv) +{ + GtkWidget *wnd; + PangoFontDescription *monospacefont; + GtkTreeModel *sortmodel; + struct passwd *pwent; + + setlocale(LC_ALL, ""); + bindtextdomain(PACKAGE, LOCALEDIR); + textdomain(PACKAGE); + gtk_init(&argc, &argv); + dc_init(); + signal(SIGCHLD, SIG_IGN); + pubhubaddr = sstrdup("http://www.neo-modus.com/PublicHubList.config"); + dcserver = sstrdup("localhost"); + if((pwent = getpwuid(getuid())) == NULL) + { + fprintf(stderr, "could not get your passwd data"); + exit(1); + } + connectas = sstrdup(pwent->pw_name); + wnd = create_main_wnd(); + initchattags(); + + fnmodel = gtk_list_store_new(6, G_TYPE_INT, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_fnetnodes), GTK_TREE_MODEL(fnmodel)); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_chatnodes), GTK_TREE_MODEL(fnmodel)); + + pubhubmodel = gtk_list_store_new(4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT); + sortmodel = gtk_tree_model_sort_new_with_model(GTK_TREE_MODEL(pubhubmodel)); + gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(sortmodel), 3, GTK_SORT_DESCENDING); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_phublist), GTK_TREE_MODEL(sortmodel)); + g_object_unref(sortmodel); + + dlmodel = gtk_list_store_new(12, G_TYPE_INT, /* id */ + G_TYPE_INT, /* dir */ + G_TYPE_INT, /* state */ + G_TYPE_STRING, /* peerid */ + G_TYPE_STRING, /* peernick */ + G_TYPE_STRING, /* path */ + G_TYPE_INT, /* size */ + G_TYPE_INT, /* curpos */ + G_TYPE_STRING, /* stock */ + G_TYPE_FLOAT, /* percentage */ + G_TYPE_INT, /* error */ + G_TYPE_INT); /* errortime */ + gtk_tree_view_set_model(GTK_TREE_VIEW(main_downloads), GTK_TREE_MODEL(dlmodel)); + + ulmodel = gtk_list_store_new(12, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT, G_TYPE_STRING, G_TYPE_FLOAT, G_TYPE_INT, G_TYPE_INT); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_uploads), GTK_TREE_MODEL(ulmodel)); + + srchmodel = gtk_tree_store_new(9, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, G_TYPE_INT, G_TYPE_DOUBLE, G_TYPE_INT, G_TYPE_INT); + srchmodelfilter = GTK_TREE_MODEL_FILTER(gtk_tree_model_filter_new(GTK_TREE_MODEL(srchmodel), NULL)); + gtk_tree_model_filter_set_visible_func(srchmodelfilter, srchfilterfunc, NULL, NULL); + sortmodel = gtk_tree_model_sort_new_with_model(GTK_TREE_MODEL(srchmodelfilter)); + gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(sortmodel), 4, GTK_SORT_DESCENDING); + gtk_tree_view_set_model(GTK_TREE_VIEW(main_srchres), GTK_TREE_MODEL(sortmodel)); + g_object_unref(sortmodel); + + monospacefont = pango_font_description_from_string("Monospace 10"); + gtk_widget_modify_font(main_chatview, monospacefont); + pango_font_description_free(monospacefont); + readconfigfile(); + updatesbar(_("Disconnected")); + gtk_widget_show(wnd); + if(autoconn) + dcconnect(dcserver); + g_timeout_add(500, srchstatupdatecb, NULL); + g_timeout_add(5000, ksupdatecb, NULL); + gtk_main(); + return(0); +} diff --git a/clients/gtk2/mainwnd.desc b/clients/gtk2/mainwnd.desc new file mode 100644 index 0000000..c5d653e --- /dev/null +++ b/clients/gtk2/mainwnd.desc @@ -0,0 +1,176 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +;prefix: main_ +;hasaccels: y +;hascolumns: y +;hasrenderers: y +:wnd name: wnd title: "Dolda Connect" var: y sig(delete_event):initdeath + :vbox + :menubar + :menuitem label: _Main + :menu + $menuitem name: connmenu label: _Connect sig: activate accel: "CONTROL+C" var: y + $menuitem name: dconnmenu label: _Disconnect sig: activate accel: "CONTROL+D" var: y sensitive: FALSE + $menusep + $menuitem name: sdmenu label: "_Shut down daemon" sig: activate + $smenuitem stock: QUIT sig(activate):initdeath + end + end + :menuitem label: Op_tions + :menu + $smenuitem name: prefmenu stock: PREFERENCES sig: activate + end + end + end + :vpaned name: pane1 var: y expand: TRUE fill: TRUE resize: 1 + :notebook + :vpaned name: pane2 var: y nblabel: "_Hub connections" resize: 2 + :vbox + $mlbl label: "Connected hu_bs" mwidget: fnetnodes + :sw expand: TRUE fill: TRUE + :treeview name: fnetnodes var: y rules: TRUE + :tvcol title: "Hub name" sortcol: 1 resizable: TRUE + $pixbufrend stock_id: 4 + $textrend text: 1 + end + $tvcol title: "# users" text: 3 sortcol: 3 resizable: TRUE + end + end + $btn name: dcnctbtn label: "D_isconnect" sig: clicked + end + :vbox + $mlbl label: "_Public hub list" mwidget: phublist + :sw expand: TRUE fill: TRUE + :treeview name: phublist var: y rules: TRUE searchcol: 0 sig(cursor-changed): cb_main_phublist_cchange sig(row-activated): cb_main_phublist_activate + $tvcol title: "# users" text: 3 sortcol: 3 resizable: TRUE + $tvcol title: "Name" text: 0 sortcol: 0 resizable: TRUE + $tvcol title: "Description" text: 2 sortcol: 2 resizable: TRUE + end + end + :table rows: 2 cols: 3 fill: TRUE + $mlbl label: "_Filter:" tx: 0 ty: 0 mwidget: pubhubfilter + $text name: pubhubfilter var: y expand: y fill: y sig: activate tx: 1 ty: 0 + $btn label: "_Get public hub list" sig(clicked): cb_main_pubhubfilter_activate tx: 2 ty: 0 + $mlbl label: "_Address:" tx: 0 ty: 1 mwidget: fnaddr + $text name: fnaddr var: y expand: y fill: y sig: activate tx: 1 ty: 1 + $btn label: "C_onnect" sig(clicked): cb_main_fnaddr_activate tx: 2 ty: 1 + end + end + end + :vpaned name: pane3 var: y nblabel: "_Chat" resize: 2 + :vbox + $mlbl label: "Hu_bs" mwidget: chatnodes + :sw fill: TRUE expand: TRUE + :treeview name: chatnodes var: y rules: TRUE sig(row-activated): cb_main_chatnodes_activate + :tvcol title: "Hub name" sortcol: 1 + $pixbufrend stock_id: 5 + $textrend text: 1 + end + end + end + end + :vbox + :sw fill: TRUE expand: TRUE + $textview name: chatview var: y editable: FALSE + end + :hbox + $mlbl label: "Chat st_ring:" mwidget: chatstr + $text name: chatstr var: y expand: TRUE fill: TRUE sig: activate + $btn label: "S_end" sig(clicked): cb_main_chatstr_activate + end + end + end + :vbox nblabel: "_Search" + :hbox + $mlbl label: "S_imple search:" mwidget: simplesrch + $text name: simplesrch var: y expand: TRUE fill: TRUE sig: changed sig(activate): cb_main_srchbtn_clicked + $btn name: srchbtn var: y label: "S_earch" sig: clicked + $btn name: srchcanbtn var: y label: "C_ancel" sig: clicked sensitive: FALSE + end + $chk name: filternoslots var: y label: "Displa_y results with free slots only" sig: toggled + :exp label: "Ad_vanced" name: advexp var: y + :table rows: 2 cols: 2 + $mlbl tx: 0 ty: 0 label: "C_omplete search expression:" mwidget: realsrch + $text tx: 1 ty: 0 name: realsrch var: y expand: y fill: y sig: changed sig(activate): cb_main_srchbtn_clicked + $mlbl tx: 0 ty: 1 label: "Filter ar_gument:" mwidget: dlarg + $text tx: 1 ty: 1 name: dlarg var: y expand: y fill: y + end + end + $lbl name: srchstatus var: y label: "Ready to search" fill: TRUE + $hr pad: 5 + $mlbl label: "Search _results:" mwidget: srchres + :sw expand: TRUE fill: TRUE + :treeview name: srchres var: y rules: TRUE searchcol: 3 sig(row-activated): cb_main_srchres_activate + :tvcol title: "#" sortcol: 7 resizable: FALSE + $textrend func: hidezerofunc funcdata: "(gpointer)7" + end + $tvcol title: "Peer name" text: 2 sortcol: 2 resizable: TRUE + $tvcol title: "File name" text: 3 sortcol: 3 resizable: TRUE expander: y + :tvcol title: "Size" sortcol: 4 resizable: TRUE + $textrend func: transnicebytefunc funcdata: "(gpointer)4" + end + $tvcol title: "Slots" text: 5 sortcol: 5 resizable: TRUE + :tvcol title: "Known speed" sortcol: 8 resizable: TRUE + $textrend func: speedtimefunc + end + $tvcol title: "Rsp. time" text: 6 sortcol: 6 resizable: TRUE + end + end + end + end + :notebook + :vbox nblabel: "_Downloads" + $mlbl label: "_List of downloads:" mwidget: downloads + :sw fill: TRUE expand: TRUE + :treeview name: downloads var: y sig(key-press-event): cb_main_trlist_keypress + :tvcol title: "User Name" + $pixbufrend stock_id: 8 + $textrend text: 4 + end + $tvcol title: "File Name" text: 5 + :tvcol title: "Size" + $textrend func: transnicebytefunc funcdata: "(gpointer)6" + end + :tvcol title: "Position" +#ifdef ENABLE_GTK2PBAR + $custrend newfunc: custom_cell_renderer_progress_new attr(percentage): 9 +#else + $textrend func: percentagefunc funcdata: "(gpointer)9" expand: FALSE +#endif + $textrend func: transnicebytefunc funcdata: "(gpointer)7" + end + :tvcol title: "Error" + $textrend func: transerrorinfo + end + end + end + end + :vbox nblabel: "_Uploads" + $mlbl label: "_List of uploads:" mwidget: uploads + :sw fill: TRUE expand: TRUE + :treeview name: uploads var: y sig(key-press-event): cb_main_trlist_keypress + :tvcol title: "User Name" + $pixbufrend stock_id: 8 + $textrend text: 4 + end + $tvcol title: "File Name" text: 5 + :tvcol title: "Size" + $textrend func: transnicebytefunc funcdata: "(gpointer)6" + end + :tvcol title: "Position" +#ifdef ENABLE_GTK2PBAR + $custrend newfunc: custom_cell_renderer_progress_new attr(percentage): 9 +#else + $textrend func: percentagefunc funcdata: "(gpointer)9" expand: FALSE +#endif + $textrend func: transnicebytefunc funcdata: "(gpointer)7" + end + end + end + end + end + end + $sbar var: y name: statusbar + end +end diff --git a/clients/gtk2/makegdesc b/clients/gtk2/makegdesc new file mode 100755 index 0000000..31cd9a0 --- /dev/null +++ b/clients/gtk2/makegdesc @@ -0,0 +1,422 @@ +#!/usr/bin/perl + +$tempvar = 0; + +sub printwidgets +{ + my($widget, $sl, $p, $sig, $cb, $data, $pf, $cpf, $mod, $key, @delayedlines); + $sl = $_[1]; + $p = " " . (" " x $sl); + $cpf = $_[2]; + @delayedlines = (); + foreach $widget (@{$_[0]}) + { + if($widget->{"type"} eq "wnd") + { + print "${p}stack[$sl] = gtk_window_new(GTK_WINDOW_TOPLEVEL);\n"; + if($options{"hasaccels"}) { + print "${p}gtk_window_add_accel_group(GTK_WINDOW(stack[$sl]), accel_group);\n"; + } + if($widget->{"title"}) { + print "${p}gtk_window_set_title(GTK_WINDOW(stack[$sl]), \"" . $widget->{"title"} . "\");\n"; + } + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_container_add(GTK_CONTAINER(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} =~ /[hv]box/) { + print "${p}stack[$sl] = gtk_" . $widget->{"type"} . "_new("; + print $widget->{"homo"}?"TRUE, ":"FALSE, "; + print $widget->{"spacing"} || "0"; + print ");\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_box_pack_start(GTK_BOX(stack[" . ($sl - 1) . "]), stack[$sl], "; + print (($widget->{"expand"} || $widget->{"parent"}->{"dexpand"})?"TRUE, ":"FALSE, "); + print (($widget->{"fill"} || $widget->{"parent"}->{"dfill"})?"TRUE, ":"FALSE, "); + print $widget->{"pad"} || "0"; + print ");\n"; + } + } elsif($widget->{"type"} eq "table") { + print "${p}stack[$sl] = gtk_table_new(" . $widget->{"rows"} . ", " . $widget->{"cols"}; + print ", " . (($widget->{"homo"} eq "TRUE")?"TRUE":"FALSE"); + print ");\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_table_attach(GTK_TABLE(stack[" . ($sl - 1) . "]), stack[$sl]"; + print ", " . $widget->{"tx"}; + print ", " . ($widget->{"tx"} + (defined($widget->{"tw"})?$widget->{"tw"}:1)); + print ", " . $widget->{"ty"}; + print ", " . ($widget->{"ty"} + (defined($widget->{"th"})?$widget->{"th"}:1)); + if($widget->{"fill"} eq "y") { + $widget->{"fillx"} = "y"; + $widget->{"filly"} = "y"; + } + if($widget->{"shrink"} eq "y") { + $widget->{"shrinkx"} = "y"; + $widget->{"shrinky"} = "y"; + } + if($widget->{"expand"} eq "y") { + $widget->{"expandx"} = "y"; + $widget->{"expandy"} = "y"; + } + print ", 0"; + print " | GTK_FILL" if $widget->{"fillx"} eq "y"; + print " | GTK_SHRINK" if $widget->{"shrinkx"} eq "y"; + print " | GTK_EXPAND" if $widget->{"expandx"} eq "y"; + print ", 0"; + print " | GTK_FILL" if $widget->{"filly"} eq "y"; + print " | GTK_SHRINK" if $widget->{"shrinky"} eq "y"; + print " | GTK_EXPAND" if $widget->{"expandy"} eq "y"; + print ", " . (defined($widget->{"padx"})?$widget->{"padx"}:"0"); + print ", " . (defined($widget->{"pady"})?$widget->{"pady"}:"0"); + print ");\n"; + } + } elsif($widget->{"type"} eq "btn") { + $widget->{"label"} || die("Can't have button without label\n"); + print "${p}stack[$sl] = gtk_button_new_with_mnemonic(_(\"" . $widget->{"label"} . "\"));\n"; + } elsif($widget->{"type"} eq "chk") { + $widget->{"label"} || die("Can't have check button without label\n"); + print "${p}stack[$sl] = gtk_check_button_new_with_mnemonic(_(\"" . $widget->{"label"} . "\"));\n"; + } elsif($widget->{"type"} eq "sbtn") { + $widget->{"stock"} || die("Can't have button without stock\n"); + print "${p}stack[$sl] = gtk_button_new_from_stock(GTK_STOCK_" . $widget->{"stock"} . ");\n"; + } elsif($widget->{"type"} eq "simg") { + $widget->{"stock"} || die("Can't have image without stock\n"); + $widget->{"size"} || die("Can't have image without size\n"); + print "${p}stack[$sl] = gtk_image_new_from_stock(GTK_STOCK_" . $widget->{"stock"} . ", GTK_ICON_SIZE_" . $widget->{"size"} . ");\n"; + } elsif($widget->{"type"} eq "lbl") { + $widget->{"label"} || die("Can't have label without label\n"); + print "${p}stack[$sl] = gtk_label_new(_(\"" . $widget->{"label"} . "\"));\n"; + } elsif($widget->{"type"} eq "mlbl") { + $widget->{"label"} || die("Can't have label without label\n"); + print "${p}stack[$sl] = gtk_label_new_with_mnemonic(_(\"" . $widget->{"label"} . "\"));\n"; + if(defined($widget->{"mwidget"})) + { + if($widget->{"var"} ne "y") { + $widget->{"var"} = "l"; + } + if(!defined($widget->{"name"})) { + $widget->{"name"} = "temp" . $tempvar++; + } + $str = "gtk_label_set_mnemonic_widget(GTK_LABEL("; + if($widget->{"var"} eq "y") { + $str .= $options{"prefix"}; + } + $str .= $widget->{"name"}; + $str .= "), " . $options{"prefix"} . $widget->{"mwidget"} . ");"; + push @delayedlines, ($str); + } + } elsif($widget->{"type"} eq "text") { + print "${p}stack[$sl] = gtk_entry_new();\n"; + if($widget->{"default"}) { + print "${p}gtk_entry_set_text(GTK_ENTRY(stack[$sl]), \"" . $widget->{"default"} . "\");\n"; + } + } elsif($widget->{"type"} eq "menubar") { + print "${p}stack[$sl] = gtk_menu_bar_new();\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_menu_shell_append(GTK_MENU_SHELL(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} eq "menuitem") { + print "${p}stack[$sl] = gtk_menu_item_new_with_mnemonic(_(\"" . $widget->{"label"} . "\"));\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_menu_item_set_submenu(GTK_MENU_ITEM(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} eq "smenuitem") { + print "${p}stack[$sl] = gtk_image_menu_item_new_from_stock(GTK_STOCK_" . $widget->{"stock"} . ", accel_group);\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_menu_item_set_submenu(GTK_MENU_ITEM(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} eq "menusep") { + print "${p}stack[$sl] = gtk_separator_menu_item_new();\n"; + } elsif($widget->{"type"} eq "menu") { + print "${p}stack[$sl] = gtk_menu_new();\n"; + if($options{"hasaccels"}) { + print "${p}gtk_menu_set_accel_group(GTK_MENU(stack[$sl]), accel_group);\n"; + } + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_menu_shell_append(GTK_MENU_SHELL(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + }; + $widget->{"noshow"} = 1; + } elsif($widget->{"type"} =~ /^[hv]paned$/) { + print "${p}stack[$sl] = gtk_" . $widget->{"type"} . "_new();\n"; + $widget->{"cur"} = 1; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_paned_pack" . ($widget->{"parent"}->{"cur"}) . "(GTK_PANED(stack[" . ($sl - 1) . "]), stack[$sl]"; + print ", " . ((index($widget->{"parent"}->{"resize"}, $widget->{"parent"}->{"cur"}) < 0)?"FALSE":"TRUE"); + print ", " . ((index($widget->{"parent"}->{"shrink"}, $widget->{"parent"}->{"cur"}) < 0)?"FALSE":"TRUE"); + print ");\n"; + $widget->{"parent"}->{"cur"}++; + } + } elsif($widget->{"type"} eq "notebook") { + print "${p}stack[$sl] = gtk_notebook_new();\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_notebook_append_page(GTK_NOTEBOOK(stack[" . ($sl - 1) . "]), stack[$sl]"; + print ", gtk_label_new_with_mnemonic(_(\"" . $widget->{"nblabel"} . "\"))"; + print ");\n"; + } + } elsif($widget->{"type"} eq "sw") { + print "${p}stack[$sl] = gtk_scrolled_window_new(NULL, NULL);\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_container_add(GTK_CONTAINER(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} eq "frame") { + print "${p}stack[$sl] = gtk_frame_new(_(\"" . $widget->{"label"} . "\"));\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_container_add(GTK_CONTAINER(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} eq "exp") { + print "${p}stack[$sl] = gtk_expander_new_with_mnemonic(_(\"" . $widget->{"label"} . "\"));\n"; + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_container_add(GTK_CONTAINER(stack[" . ($sl - 1) . "]), stack[$sl]);\n"; + } + } elsif($widget->{"type"} eq "treeview") { + print "${p}stack[$sl] = gtk_tree_view_new();\n"; + if(defined($widget->{"hvis"})) { + print "${p}gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(stack[$sl]), " . $widget->{"hvis"} . ");\n"; + } + if(defined($widget->{"rules"})) { + print "${p}gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(stack[$sl]), " . $widget->{"rules"} . ");\n"; + } + if(defined($widget->{"searchcol"})) { + print "${p}gtk_tree_view_set_search_column(GTK_TREE_VIEW(stack[$sl]), " . $widget->{"searchcol"} . ");\n"; + print "${p}gtk_tree_view_set_enable_search(GTK_TREE_VIEW(stack[$sl]), TRUE);\n"; + } + $pf = sub + { + my($widget, $p, $sl) = @_; + print "${p}gtk_tree_view_append_column(GTK_TREE_VIEW(stack[" . ($sl - 1) . "]), column);\n"; + if($widget->{"expander"} eq "y") { + print "${p}gtk_tree_view_set_expander_column(GTK_TREE_VIEW(stack[" . ($sl - 1) . "]), column);\n"; + } + } + } elsif($widget->{"type"} eq "tvcol") { + if(!defined($widget->{"subwidgets"})) + { + print "${p}column = gtk_tree_view_column_new_with_attributes("; + print "_(\"" . $widget->{"title"} . "\")"; + print ", gtk_cell_renderer_text_new()"; + if(defined($widget->{"text"})) { + print ", \"text\", " . $widget->{"text"}; + } + print ", NULL);\n"; + } else { + print "${p}column = gtk_tree_view_column_new();\n"; + print "${p}gtk_tree_view_column_set_title(column, _(\"" . $widget->{"title"} . "\"));\n"; + } + if(defined($widget->{"sortcol"})) { + print "${p}gtk_tree_view_column_set_sort_column_id(column, " . $widget->{"sortcol"} . ");\n"; + } + if(defined($widget->{"resizable"})) { + print "${p}gtk_tree_view_column_set_resizable(column, " . $widget->{"resizable"} . ");\n"; + } + $widget->{"noshow"} = 1; + $pf = sub + { + } + } elsif($widget->{"type"} eq "textrend") { + print "${p}renderer = gtk_cell_renderer_text_new();\n"; + print "${p}gtk_tree_view_column_pack_start(column, renderer, " . (defined($widget->{"expand"})?$widget->{"expand"}:"TRUE") . ");\n"; + if(defined($widget->{"text"})) { + print "${p}gtk_tree_view_column_add_attribute(column, renderer, \"text\", " . $widget->{"text"} . ");\n"; + } + if(defined($widget->{"func"})) { + print "${p}gtk_tree_view_column_set_cell_data_func(column, renderer, " . $widget->{"func"} . ", " . ($widget->{"funcdata"} || "NULL") . ", NULL);\n"; + } + $widget->{"noshow"} = 1; + } elsif($widget->{"type"} eq "custrend") { + print "${p}renderer = GTK_CELL_RENDERER(" . $widget->{"newfunc"} . "());\n"; + print "${p}gtk_tree_view_column_pack_start(column, renderer, " . (defined($widget->{"expand"})?$widget->{"expand"}:"FALSE") . ");\n"; + foreach $attr (keys %{$widget}) + { + if($attr =~ /attr\((\S+)\)/) + { + print "${p}gtk_tree_view_column_add_attribute(column, renderer, \"" . $1 . "\", " . $widget->{$attr} . ");\n"; + } + } + $widget->{"noshow"} = 1; + } elsif($widget->{"type"} eq "pixbufrend") { + print "${p}renderer = gtk_cell_renderer_pixbuf_new();\n"; + print "${p}gtk_tree_view_column_pack_start(column, renderer, FALSE);\n"; + if(defined($widget->{"stock_id"})) { + print "${p}gtk_tree_view_column_add_attribute(column, renderer, \"stock_id\", " . $widget->{"stock_id"} . ");\n"; + } + $widget->{"noshow"} = 1; + } elsif($widget->{"type"} eq "textview") { + print "${p}stack[$sl] = gtk_text_view_new();\n"; + if(defined($widget->{"editable"})) { + print "${p}gtk_text_view_set_editable(GTK_TEXT_VIEW(stack[$sl]), " . $widget->{"editable"} . ");\n"; + } + } elsif($widget->{"type"} eq "hr") { + print "${p}stack[$sl] = gtk_hseparator_new();\n"; + } elsif($widget->{"type"} eq "sbar") { + print "${p}stack[$sl] = gtk_statusbar_new();\n"; + if($widget->{"grip"} eq "n") { + print "${p}gtk_statusbar_set_has_resize_grip(GTK_STATUSBAR(stack[$sl]), FALSE);\n"; + } + } else { + print STDERR "Unknown widget: " . $widget->{"type"} ."\n"; + } + if($widget->{"sensitive"}) { + print "${p}gtk_widget_set_sensitive(stack[$sl], " . $widget->{"sensitive"} . ");\n"; + } + if($widget->{"var"} eq "y") { + print $p . $options{"prefix"} . $widget->{"name"} . " = stack[$sl];\n"; + } + if($widget->{"var"} eq "l") { + print $p . "GtkWidget *" . $widget->{"name"} . " = stack[$sl];\n"; + } + if($widget->{"sig"}) + { + while($widget->{"sig"} =~ /\G([\w_]+),?/g) { + print "${p}g_signal_connect(G_OBJECT(stack[$sl]), \"$1\", G_CALLBACK(cb_" . $options{"prefix"} . $widget->{"name"} . "_" . $1 . "), (gpointer)NULL);\n"; + } + } + if($widget->{"accel"}) + { + $mod = ""; + while($widget->{"accel"} =~ /\G(\w+)\+/gc) + { + $mod .= " | " if($mod); + $mod = $mod . "GDK_" . $1 . "_MASK"; + } + $mod || ($mod = "0"); + $widget->{"accel"} =~ /\G(\w+)/g; + $key = $1; + print "${p}gtk_widget_add_accelerator(stack[$sl], \"activate\", accel_group, GDK_$key, $mod, GTK_ACCEL_VISIBLE);\n"; + } + foreach $attr (keys %{$widget}) + { + if($attr =~ /^sig\((\S+)\)/) + { + $sig = $1; + if($widget->{$attr} =~ /([^,]*),(.*)/) + { + $cb = $1; + $data = $2; + } else { + $cb = $widget->{$attr}; + $data = "NULL"; + } + print "${p}g_signal_connect(G_OBJECT(stack[$sl]), \"$1\", G_CALLBACK($cb), (gpointer)$data);\n"; + } + } + if($widget->{"subwidgets"}) + { + print "$p\n"; + printwidgets($widget->{"subwidgets"}, $sl + 1, $pf); + } + if($sl > 0) + { + &$cpf($widget, $p, $sl); + if(!$widget->{"noshow"}) { + print "${p}gtk_widget_show(stack[$sl]);\n"; + } + } + print "$p\n"; + } + foreach $line (@delayedlines) + { + print $p . $line . "\n"; + } +} + +sub printvars +{ + my($widget); + foreach $widget (@{$_[0]}) + { + if($widget->{"var"}) + { + print "GtkWidget *" . $options{"prefix"} . $widget->{"name"} .";\n"; + } + printvars($widget->{"subwidgets"}) if($widget->{"subwidgets"}); + } +} + +sub dequote +{ + my($text); + ($text) = @_; + $text =~ s/([^\\]|^)\"/$1/g; + $text =~ s/\\(.)/$1/g; + return $text; +} + +$rootwidgets = []; +@estack = ($rootwidgets); +@wstack = (); +$curwidget = 0; +$maxstack = 1; + +while(<>) +{ + chomp; + s/(^|\s+)\#.*$//; + s/^\s*//; + if(/^;\s*(\w+)\s*:\s*(\w.*)/) + { + $options{$1} = $2; + } elsif(/^([:\$])\s*(\w+)/g) { + $curwidget = {"type" => $2}; + push @{$estack[0]}, $curwidget; + if((scalar @wstack) > 0) { + $curwidget->{"parent"} = $wstack[0]; + } + if($1 eq ":") + { + unshift @estack, ($curwidget->{"subwidgets"} = []); + unshift @wstack, $curwidget; + } + $maxstack = (scalar @estack) if((scalar @estack) > $maxstack); + while(/\G\s*(\S+)\s*:\s*((\w+|\"([^\\\"]+|\\.)*([^\\]|)\"|\\.)+)/g) + { + $curwidget->{$1} = dequote($2); + } + } elsif(/^%\s*(\S+)\s*:\s*((\w+|\"([^\\\"]+|\\.)*([^\\]|)\"|\\.)+)/) { + $curwidget || die("No current widget\n"); + $curwidget->{$1} = dequote($2); + } elsif(/^end/) { + shift @estack; + shift @wstack; + $curwidget = $wstack[0] if((scalar @wstack) > 0); + } elsif(!$_) { + } else { + print STDERR "Invalid construct: $_\n"; + } +} + +printvars $rootwidgets; +print "\n"; +print "GtkWidget *create_" . $options{"prefix"} . "wnd(void)\n"; +print "{\n"; +print " GtkWidget *stack[$maxstack];\n"; +print " GtkAccelGroup *accel_group;\n" if $options{"hasaccels"}; +print " GtkTreeViewColumn *column;\n" if $options{"hascolumns"}; +print " GtkCellRenderer *renderer;\n" if $options{"hasrenderers"}; +print " \n"; +print " accel_group = gtk_accel_group_new();\n" if $options{"hasaccels"}; +printwidgets $rootwidgets, 0; +print " return(stack[0]);\n"; +print "}\n"; diff --git a/clients/gtk2/pref.desc b/clients/gtk2/pref.desc new file mode 100644 index 0000000..f14aaeb --- /dev/null +++ b/clients/gtk2/pref.desc @@ -0,0 +1,10 @@ +;prefix: pref_ +:table rows: 4 cols: 2 + $mlbl label: "_Public hub list URL:" tx: 0 ty: 0 mwidget: pubhuburl + $text name: pubhuburl var: y expand: y fill: y tx: 1 ty: 0 + $mlbl label: "_Dolda connect user name:" tx: 0 ty: 1 mwidget: connectas + $text name: connectas var: y expand: y fill: y tx: 1 ty: 1 + $mlbl label: "Dolda Connect _server:" tx: 0 ty: 2 mwidget: dcserver + $text name: dcserver var: y expand: y fill: y tx: 1 ty: 2 + $chk name: autoconn var: y label: "Connect _automatically on startup" tx: 0 ty: 3 tw: 2 +end diff --git a/clients/gtk2/progressbar.c b/clients/gtk2/progressbar.c new file mode 100644 index 0000000..2dff9b2 --- /dev/null +++ b/clients/gtk2/progressbar.c @@ -0,0 +1,344 @@ +/* Taken from the GTK TreeView tutorial on gtk.org. + * Slightly modified. + */ + +#include "progressbar.h" + +/* This is based mainly on GtkCellRendererProgress + * in GAIM, written and (c) 2002 by Sean Egan + * (Licensed under the GPL), which in turn is + * based on Gtk's GtkCellRenderer[Text|Toggle|Pixbuf] + * implementation by Jonathan Blandford */ + +/* Some boring function declarations: GObject type system stuff */ + +static void custom_cell_renderer_progress_init (CustomCellRendererProgress *cellprogress); + +static void custom_cell_renderer_progress_class_init (CustomCellRendererProgressClass *klass); + +static void custom_cell_renderer_progress_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *pspec); + +static void custom_cell_renderer_progress_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec); + +static void custom_cell_renderer_progress_finalize (GObject *gobject); + + +/* These functions are the heart of our custom cell renderer: */ + +static void custom_cell_renderer_progress_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height); + +static void custom_cell_renderer_progress_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + guint flags); + + +enum +{ + PROP_PERCENTAGE = 1, +}; + +static gpointer parent_class; + + +/*************************************************************************** + * + * custom_cell_renderer_progress_get_type: here we register our type with + * the GObject type system if we + * haven't done so yet. Everything + * else is done in the callbacks. + * + ***************************************************************************/ + +GType +custom_cell_renderer_progress_get_type (void) +{ + static GType cell_progress_type = 0; + + if (cell_progress_type) + return cell_progress_type; + + if (1) + { + static const GTypeInfo cell_progress_info = + { + sizeof (CustomCellRendererProgressClass), + NULL, /* base_init */ + NULL, /* base_finalize */ + (GClassInitFunc) custom_cell_renderer_progress_class_init, + NULL, /* class_finalize */ + NULL, /* class_data */ + sizeof (CustomCellRendererProgress), + 0, /* n_preallocs */ + (GInstanceInitFunc) custom_cell_renderer_progress_init, + }; + + /* Derive from GtkCellRenderer */ + cell_progress_type = g_type_register_static (GTK_TYPE_CELL_RENDERER, + "CustomCellRendererProgress", + &cell_progress_info, + 0); + } + + return cell_progress_type; +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_init: set some default properties of the + * parent (GtkCellRenderer). + * + ***************************************************************************/ + +static void +custom_cell_renderer_progress_init (CustomCellRendererProgress *cellrendererprogress) +{ + GTK_CELL_RENDERER(cellrendererprogress)->mode = GTK_CELL_RENDERER_MODE_INERT; + GTK_CELL_RENDERER(cellrendererprogress)->xpad = 2; + GTK_CELL_RENDERER(cellrendererprogress)->ypad = 2; +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_class_init: + * + * set up our own get_property and set_property functions, and + * override the parent's functions that we need to implement. + * And make our new "percentage" property known to the type system. + * If you want cells that can be activated on their own (ie. not + * just the whole row selected) or cells that are editable, you + * will need to override 'activate' and 'start_editing' as well. + * + ***************************************************************************/ + +static void +custom_cell_renderer_progress_class_init (CustomCellRendererProgressClass *klass) +{ + GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS(klass); + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + parent_class = g_type_class_peek_parent (klass); + object_class->finalize = custom_cell_renderer_progress_finalize; + + /* Hook up functions to set and get our + * custom cell renderer properties */ + object_class->get_property = custom_cell_renderer_progress_get_property; + object_class->set_property = custom_cell_renderer_progress_set_property; + + /* Override the two crucial functions that are the heart + * of a cell renderer in the parent class */ + cell_class->get_size = custom_cell_renderer_progress_get_size; + cell_class->render = custom_cell_renderer_progress_render; + + /* Install our very own properties */ + g_object_class_install_property (object_class, + PROP_PERCENTAGE, + g_param_spec_double ("percentage", + "Percentage", + "The fractional progress to display", + 0, 1, 0, + G_PARAM_READWRITE)); +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_finalize: free any resources here + * + ***************************************************************************/ + +static void +custom_cell_renderer_progress_finalize (GObject *object) +{ +/* + CustomCellRendererProgress *cellrendererprogress = CUSTOM_CELL_RENDERER_PROGRESS(object); +*/ + + /* Free any dynamically allocated resources here */ + + (* G_OBJECT_CLASS (parent_class)->finalize) (object); +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_get_property: as it says + * + ***************************************************************************/ + +static void +custom_cell_renderer_progress_get_property (GObject *object, + guint param_id, + GValue *value, + GParamSpec *psec) +{ + CustomCellRendererProgress *cellprogress = CUSTOM_CELL_RENDERER_PROGRESS(object); + + switch (param_id) + { + case PROP_PERCENTAGE: + g_value_set_double(value, cellprogress->progress); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, psec); + break; + } +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_set_property: as it says + * + ***************************************************************************/ + +static void +custom_cell_renderer_progress_set_property (GObject *object, + guint param_id, + const GValue *value, + GParamSpec *pspec) +{ + CustomCellRendererProgress *cellprogress = CUSTOM_CELL_RENDERER_PROGRESS (object); + + switch (param_id) + { + case PROP_PERCENTAGE: + cellprogress->progress = g_value_get_double(value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec); + break; + } +} + +/*************************************************************************** + * + * custom_cell_renderer_progress_new: return a new cell renderer instance + * + ***************************************************************************/ + +GtkCellRenderer * +custom_cell_renderer_progress_new (void) +{ + return g_object_new(CUSTOM_TYPE_CELL_RENDERER_PROGRESS, NULL); +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_get_size: crucial - calculate the size + * of our cell, taking into account + * padding and alignment properties + * of parent. + * + ***************************************************************************/ + +#define FIXED_WIDTH 100 +#define FIXED_HEIGHT 12 + +static void +custom_cell_renderer_progress_get_size (GtkCellRenderer *cell, + GtkWidget *widget, + GdkRectangle *cell_area, + gint *x_offset, + gint *y_offset, + gint *width, + gint *height) +{ + gint calc_width; + gint calc_height; + + calc_width = (gint) cell->xpad * 2 + FIXED_WIDTH; + calc_height = (gint) cell->ypad * 2 + FIXED_HEIGHT; + + if (width) + *width = calc_width; + + if (height) + *height = calc_height; + + if (cell_area) + { + if (x_offset) + { + *x_offset = cell->xalign * (cell_area->width - calc_width); + *x_offset = MAX (*x_offset, 0); + } + + if (y_offset) + { + *y_offset = cell->yalign * (cell_area->height - calc_height); + *y_offset = MAX (*y_offset, 0); + } + } +} + + +/*************************************************************************** + * + * custom_cell_renderer_progress_render: crucial - do the rendering. + * + ***************************************************************************/ + +static void +custom_cell_renderer_progress_render (GtkCellRenderer *cell, + GdkWindow *window, + GtkWidget *widget, + GdkRectangle *background_area, + GdkRectangle *cell_area, + GdkRectangle *expose_area, + guint flags) +{ + CustomCellRendererProgress *cellprogress = CUSTOM_CELL_RENDERER_PROGRESS (cell); + GtkStateType state; + gint width, height; + gint x_offset, y_offset; + + custom_cell_renderer_progress_get_size (cell, widget, cell_area, + &x_offset, &y_offset, + &width, &height); + + if (GTK_WIDGET_HAS_FOCUS (widget)) + state = GTK_STATE_ACTIVE; + else + state = GTK_STATE_NORMAL; + + width -= cell->xpad*2; + height -= cell->ypad*2; + + gtk_paint_box (widget->style, + window, + GTK_STATE_NORMAL, GTK_SHADOW_IN, + NULL, widget, "trough", + cell_area->x + x_offset + cell->xpad, + cell_area->y + y_offset + cell->ypad, + width - 1, height - 1); + + gtk_paint_box (widget->style, + window, + state, GTK_SHADOW_OUT, + NULL, widget, "bar", + cell_area->x + x_offset + cell->xpad, + cell_area->y + y_offset + cell->ypad, + width * cellprogress->progress, + height - 1); +} diff --git a/clients/gtk2/progressbar.h b/clients/gtk2/progressbar.h new file mode 100644 index 0000000..4b773f3 --- /dev/null +++ b/clients/gtk2/progressbar.h @@ -0,0 +1,44 @@ +/* Taken from the GTK TreeView tutorial on gtk.org. */ + +#ifndef _custom_cell_renderer_progressbar_included_ +#define _custom_cell_renderer_progressbar_included_ + +#include + +/* Some boilerplate GObject type check and type cast macros. + * 'klass' is used here instead of 'class', because 'class' + * is a c++ keyword */ + +#define CUSTOM_TYPE_CELL_RENDERER_PROGRESS (custom_cell_renderer_progress_get_type()) +#define CUSTOM_CELL_RENDERER_PROGRESS(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), CUSTOM_TYPE_CELL_RENDERER_PROGRESS, CustomCellRendererProgress)) +#define CUSTOM_CELL_RENDERER_PROGRESS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CUSTOM_TYPE_CELL_RENDERER_PROGRESS, CustomCellRendererProgressClass)) +#define CUSTOM_IS_CELL_PROGRESS_PROGRESS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CUSTOM_TYPE_CELL_RENDERER_PROGRESS)) +#define CUSTOM_IS_CELL_PROGRESS_PROGRESS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CUSTOM_TYPE_CELL_RENDERER_PROGRESS)) +#define CUSTOM_CELL_RENDERER_PROGRESS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CUSTOM_TYPE_CELL_RENDERER_PROGRESS, CustomCellRendererProgressClass)) + +typedef struct _CustomCellRendererProgress CustomCellRendererProgress; +typedef struct _CustomCellRendererProgressClass CustomCellRendererProgressClass; + +/* CustomCellRendererProgress: Our custom cell renderer + * structure. Extend according to need */ + +struct _CustomCellRendererProgress +{ + GtkCellRenderer parent; + gdouble progress; +}; + + +struct _CustomCellRendererProgressClass +{ + GtkCellRendererClass parent_class; +}; + + +GType custom_cell_renderer_progress_get_type (void); + +GtkCellRenderer *custom_cell_renderer_progress_new (void); + + +#endif /* _custom_cell_renderer_progressbar_included_ */ + diff --git a/clients/hellodolda.jpg b/clients/hellodolda.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cab979a30791f87f73d33903e832ef412a78afd7 GIT binary patch literal 264133 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

Nt;k0aFsayx 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%0A$bqf+@EUyl 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 z zQ?3?*QXE`0ID*?tLXzVxDH%tSyK?5ra$1X?^1UxJvV;^Gogu?Qt0_u*kG_(}cP^DD zfug38#8|YXs?9A<@ukH(g6=l9oLf$zZMxdj+l{!AhZ|Z6O3<_@1tn=vDNsBG%M0V5z3__HMAxLY!g!^r!yh1sXV5>lK zPE@IvEEJ(ls;Mbi*87Z98A4J1mfMjXJO^1Q+^Z92THC{GEym4Mz}?76snUXq2LZn# z1FKTNABo2!zOrt5xT8dwk#ZqT_9EP_EOWEI&u*pCPSW~s zs_BhROtfW8-1^I7ELP;UmCJOkrrJ#wsHWq=tS$9AgtHB?8mzUU^?xny8xkDI2O*^1 z!o=T~pNKv`8*hjTrB15QX0D%Cg(9CZv+pw{1qDgI?^w%%?Y_#0ArG>*8*plcsA7Dj z)hTtzh@VKE8le$(v$5O?-CpP=4x>jIc_5AEUqf!UpjrXNlI&7B3xnx@k3+x5mn=5B z7!YnR`g8R3-{*W@uf(;@w($e-ov*m*{jk*ZVboao{5*R49)ApC%(5+e>HBl|cMd09 z8(Vy><>hTV`QG~9k8PCvU5$swsO9PP^YGoF=+?g1A3gs7PQ1mo#qjiUHX_|G$Zvlw ziTmFT55@!&Z>O)z^YiK!7?*w9lkxEIJib=rf~#x0UjEL~bhz!|Zd?h)l=N|9akl=W z!uxoA+W2OX7oCTox8?hF>0&m$AC5)H9#*&0%zg7VEblSTYmM$Y^X4zV%crL6ape(f zA3t8T?2(h+0<^jno=z4IHwSo5h-tC1}xnUvj;NL(=C*xNO3A+WJh;g zEV@%?W4w|#R)r;3N{V+~X+V^MRD{$F#XzZLD#D>jQj+6JOo%g?-fc1&3%fE{P&>`I zl_twbQClw|NeWO2TBh4V^^%*dSx5PZX{Z^diX+6$G?w^_EJ}GU`@KR6mP*x4swIb= zjN7YQ(H&|;CtQEs(K1(TKGm*mwd_Ywe*XZy?}#yaetQ|hl&G`ZPv7*m(3zsaKHVPS4OCk|0U=6No0fzP z*5BQ52GJp=!YK^frpM?{<_}*VUGUE#n}fHHhd_Ftvti+jsrH88?k#c8an9a19=K(F zyAnJ)+k13B52*6P1~c3>?B(J6e7o@_IFZl#+Sj$Ozrx#ic$_gl-HF@Q-XGiYweY_g-WENaH0<^8<}gWVvEjzR z^X%pF;xDHot*<4mjk<5kn6~4`mgD3+aK`gVA#JS*TWd;?q%9#xQd9_1RFiO0p>R?~ zh$Iou7i;bF^0^;=T>9;X2`yNkQb+6K$n)~tVR|#S1CK3q(COLUe!YVP+)eHe3v=i3 z`E<3oP{2_tQ81oE(1}?qSdN!fv#BSpah^S) za(J(01lOZC#Zk)ZT%{C(R_!K9ZMuVhg*R8bsVq(kT7p_-iA5uc(|iuasmYbIT%ksN z#qUdv{myA7>Qn5cHg#9jm)cWrD1Z~zEomQw;+g0Cz5SfblOnX<_xPd zxT}@Ehn0QcZEZe|eK!4nL%qE5)jGYG#$?ls;Y+bDnly>A!c)YhOQe)lVm6VryXg>{ zWT#GAL7K5 z9!q-v0LDjk4z^B*`H}(RIQU`ZQik04Qy{!9%=nMQcv|;vODbu6#qNcj0UC}s#3kb= zI~!+Hkh@Ij9brpt5vm!Ttf@P`Uv|KWoTJFf>UHJupd}F9t1~$&T7zhID{F8m2~tl% z=DIqv_GrvhC0qXIt<@>1BK#{vIj=C<({K+1jiD<=z&t7!0PTjT6J%3oLa4-WwJx77 zG}x}Bkf!8Cc@MDQBE)Q+RaBc@w63XAym)bMaSFxVT|=Na#Wgq-cbDMq5+Jw*mlk&@ zf#RgNyX$|l_qjP2=WdORjPGh?eJgXm^LYpamw+GmS_fvzWh~odva$aSRl@1z(5aUz zatT2YZ@VHx^tU>}uik#dW=fM9zw~f#QqIlMQG|=p?UG`fN`cDXk&t*a5*6salTe_s zi_}xs1BO9lkN_z~)y)wN#o!33s{>Iwd%?O&7PG&9&)520$&7B7bL-75Sbr1qTEuo_ z{wpaWGAo>6nVx~XgL1ME+|7O32lQ$TmcSTZDgBezSb61x(ID8of}F2<;~$84dev18 z{+Uu;MzHi-a=Iu2uh2A=LBB!)6&Z1oST~Ee2>SbZ(A509zCU*}X(GkVu)bQ|qqTd4 zzRn+&_NAmI+(N|~PA3{|x>WG(Raa--&Qg^8ca3}S&%?*dE$3xHlEpHkM>(xz**_{Q z6K(f5+qC3Msq8ss7SY@hBnIKx<0RdstpeZK({(|1R`;uxsGT1_4tX9-hp8e*am0S) z>|!#X+EM_eKc|Ft?&RCxDqXTSXM3gDF5Yr#V#=Qkxz+Hf=F5(z66L9R+O$DBU@SP~ z9Z8p>mH?H{sWc0t+6QIAZ><@r7me5Q7(oMc(aS&D_sGfUOGuZSMU#ba7JM9vtp#op zUpFipnt2&z!L%EJaa&wj!ha~*nSoVdx?T{uL+7^E*>-_pH=ANl#NnQUN5M8nvJl#D? zk^9K22}hOXE@R%>S2S_n-r?O~ug*EJZ!cl{%bYpWd@RSU8d;`JDI@Yw0j<}D_~h^p ze}B(KhCgVu;Pz>~rIA6#HHU~fL=_QFv>8pFR>vS3j7HV#fcLu|gZAG=fW5J;zg6X< z7Kdpjm#B7l!zRcwXX{ht{OsZrPT$zMp&@!E1N|GD%WtgiB~XS?=22Yt#oI-)MLrq* z5a5KDziQhq<9fPb)q&}!^fcY1G~jp&ocn=MRZmM)GKEUo{%`EMAl6s_#Xo>b6T+txkfd&u-p)^1Oh82$ ztRT{4)tFb6D1{UykRi6;YMl<3hr~~g{2Rxm*Wm)0`OSq*BIR>fR@Sor?P10;UR=*9%`<9d<q6orbW?fPYnco&{WJi@1b(YD=+D67?Q z$n&)I(>DOQZtnT}#;0@IVd6XOJMf*0?CxIg4q@|`{}E{+zV|+#*9gTy{^D<2N}p<5 z?_E%_I9M#mE=7N~=<8Q^`tv&_R5#P5BRGsY47}HiBzv%e&hL+(hf<%w=v4#Y#{kJ@vlOWwRW#NW2~C;q@O!#syXeT;a7iO_;n5fRccLtQhKy($qNs$7yWDD zubv9Ldp>Q(U_Er05b-|h-9&5a>g2wlSnsY042fDtyGd9_TV=3{`-@Vy~-McQI zEa=nx@bq%+VT&-*N9I42dD|;-YL90@{`u@Wti@2_h`f-wv!-5qVK=ItO1n^G2Nv9N zd3kksJCBt_+&nlhM{3Txkdiy_i!wWT*kFMX?A!f`8D^~} zHZ{*a&MxN|kxs>avW!DFMQ#c#o2lSrPJzzs!-_i9qP=*XDh!bOpCY%?N` zCSB6N;>JL#nhoihNu?)OY_W0#$=Myh!L?aD{rY?UC<2Y?w%wy*B&li<4}+OQ$s-q9 zAO8@7FqCHk9&N=Q;<3CkoD+FJrmS3kDdaBt2L5OUg_V}J%O)ID>p}?HRN%4aBCrtC z)V?u71vDKIbl`%zDDQc1_3L{55+Gd`g{q;Tbi|_R`6@uqU9~lBn*OW9cqo0vv;IUi zsnI-2_~*YjpoZfD^M`ic;zXAhx8grgLgoaA`d4L^y$x}WRo`Sp`!Cj znRw#F6qftc2Xhqy2ha+^%<}e3eZ7@xTXye>#ei zVUu&Jo`awP@D$!+%E}7Rzb*+b2$X*(7a1B-uYvYX8byJII*A?Si@lNkZd%r)fYdh& zzO9V79YI1sv}O3)R4FZ4{6G2`bl0({bA&^%8>02kO3pjU1K5P|HiPt_b%8rR+9@TNT&aRw>8}&a> zNAqfe+UR5d?Qjv<>s-+=th#QC%cS zYYEkAm3vgs>DFXctX9t^r9srV$YMzvc|H`VIqEY;wAloX+lI?!>(6?39No@U&F5Um zpt+;&sN^!-3Xn9eWs4Aa@WEIi^`PuB{@vmx;dP~vwnv-HK*h04BXC^2#Bon z%-AH`Qik)QlKa-z(eR^&DI>V8!&{5fsDQYaGxUGbyAcP;rDElpjo53XE0=vqvFYrxmT&eG9i2a;-= zAEcwrxseys_0xo<**8zj4MJqQ&Z1!k!>*Lf$r8TEF+rrBjyYMwiBBXv3823G)z(%y z3{0Qeq07K1%%aVfx2%=I>@PW1zJU>g4bx3PBgatQU6%+K@4Cz8zJHRA(VacJh8-4d zyTfm4u_HHtI%TuDr0~{f$C)PAvQI%oseQ?=GS5y z>W=U*XJ&a4tFhKuvv4UUCZ0WaqG9RGts+I zJP;bfE$`+krR8vZ#u%y&Je=>Fbwu_#1-vj`hPF^dTdgAF$KCb>pA=TSO#gus9W}2g zM4z0s7;3@o9=_ZU;75;*Enfcoizj|C(T>0^xxk>zk;nej)af3lv#dLq1{C$&LusJ3%_<;YQ z-;+kZN%zAS)V%h?|HYVGtL8h@OOE5OtBB3N0p=i;RL!h9J#4ntYNpBUz{@z^tu~eq zakxnvm{F9-acN3hb0+5& z)US~KG}Zj^zMlVfG=5t#*UVnrX{b_CGJ`c~BU*xC+TQ11 zOt{OgP8GjZ)27)W^aNFKLpxTBC5X+$Osg}{jJLu5YCc*g{0~(m#KpkAtsPBCVqj_H z3+`g>Hw#S>%k~?GsqKkKr#ds@9QWD2vH_)!Ce1BGdNCAf!I9zv ztz+!NUgbx;EyjNRTw^LF&>=&239RPO7~~`sat`nngY>Jm0pXvwR#iDH_ibAHpm*?w zcCs`uHM(JCw|15bt33T6R*z5=-^p71Csy4*K2z%H`0Rg-lE|bqm=b1IJaA8nCLpZR zvGEsS92FMiNguX2<(Skz?|5HX8=9_WYGpy>6MFp2s`c_S*(S*bn)_ymC-h6OCECk(&!`>qe;0Jxf)DaaffZ#jyw_$vQXbxoPD#4532z1F6U6Eo!u)cqb7^FwY zF|>Y(g*u;BIB+L`#p?;=ENA(fc2!hresaxE^$rSSI#o)fHAu%uqVH^3)7@zy0E&}_ zmf*Li#C=S_OL#3luvn(K2`?Nd>$m5jq{BkS8P*O}=xKz8RJO?+`5P<#Dk3{LpBph< zmK{o0rUD<&_E;%1epD>`icyQcq;)YK8Lhp!ei}JDJN)-d5*O5>^vWS0N3=LUU$#G! zsIBWUq89s<#l)of^aszIj(D$=r_(Z!{GVe`(3dfj+a9rRbqE|*P8VST{LTG`9AEIj zadY2E-?eZAO~l}*jk@NQKq6vM3Rcv<%RX;lFh}*(6B2gN{?o>e5=71oKX9Wc+rFMB z*NOMRAOqRCUr-wF_TeOMEcFMfgoQOzcAj{9-kX+K|E;GxCj+f-3IB(}DqK%_vm8)B ziA)7dF35Tv+JgN1?jNQ-4?tMSV@6VS9xp4Mo*I^7Z1F19yX(^~XZt=Ii&GClDcp^0 zqZ1^f<#VE#Zct9sLU|X+_sk!9Wg7p|KOWtm;<1K8S!M0O2D=--P41No=lp=9Sv)Bv zma*gK&I9K|MSpah(V*WCB*;~IBU9h~R4&?gaoa9b{tfmSh@b#^p8mdp9+cY8t^E1& zZ3k*%C#(!-a&ApjF4DZ4Ernj~5I_i;v(JSe!(1#nfluk$8#VNx?4ucf?<6Z_%{s0v ztDnXhiB@Ip!F(H{kd1fa1iO0r0_T{iEP% z$8y^xgVEY7xq#{P1liTiL%QH!^OQ3J^Lmz_)GF`aE630{Xu5QweqZhC-5A5SYj7ac zDu)z>e%`mu#}Ca=&vakp7#Xtvl!UM{Zm5M3H^mE7U?;2NrZ#1Te7DRl*E`PM9Ene8 zND5INPgN?tHNm?qbyY@P@MqT;+a6j3J^*7!)W^6LgIPc;I_DQJDn;R$%7Sd9dBH^r zSWQXt?;O~q+h~zFlMq$A|z5qg0$-=O2BQ#3y z!-`TG3-)+FKceAwsFh7ss)i{+deZ^WSqr%kHRL`Q;c~lgSe3#x>5ZbmmQQ6?I-$up zUjPl|xm$ypm~2swI2S_hKph~DY|G3Xp83|JkW&0CEOr)cKP(~ z_8Am+46kQ{?I(j4-Pb4YRM7~7$PK)mjqa&9Qo}4-_c@5%9&H#kgn?pdNzl0iLSsHT2>42=V^x4#~ub zO)KF;RUwT~g@4{(f4NW*;EAm?u7=N+{vO9Ow7}#1cJIh+(U~h5?z5Zp6?d#WBAd!<hLg~ zl`&oA{t0yPsjxTy)y2><&Z`#z-+JRP(KqiR0`D=+Nv_oPM2y$tb$pOqwK8}k@P?8q zAV8(gspN_(eLIYZ^38S#^2^&nvkyJt?GMxYeQ&D1SH5&DsPyLJOi^Ljh={)MJ@RY* z%2$g&jSqWMy3pU?^CGt&UkERwir@0j=wtu0HU%Cqtj@?1lJu~f>5{3K!h}2_=s4&& zY#F7{7(p3GQN>CgQS6yEaTZEnth+J^%rv8OMgsXP2!jgU9;o1oPunt=M zcr|ZG`X9=7^GsKYG)3xPv%60cD6O zaz*()U#Fd-U2=FjFi5FN$tHy$5o`NVnKrR>FNR)l^mRl#a7Nz=h0j1iQBmJLr#S5! zit$=XB)GF+-bIeX?V4n83Qp3l9l{a;qU_@#COsxDTr6YR z4jXfz_@-%H4a;t-XgEr@;Cq$f9hClEeeA5l{jIdWvVDtDs@#Nibi!3T0Wg6xg#X5^ zT7$7IDN%FmJ~~mkcG}K~VWyH>arOgDQN}1;By2EOr9#t+`$^#J^Pv0i#rl}V-i(ej zNO);kFUdXWCOiNWVWr}2*;c+9YXukjW(!}t6qyi7SlRbR%`Ow%iul_HY^|QC+6P9g zcr`C2CNET2e88pshEAC`=pr@8yQ^H=yzq@^Mfndd_fTqk%B(3WMIej}qx(0zd^B21 zcj~A;@{P%;piP50QSUW9)23?)s8B8;B=v28YbGVuo*BzKa2MMprP4AVz1GKJ4T={_ zP?Sdk%#bBJBn9vHTZa!q!F;GB^+ksDjiRbv(b$kKjY(adKsb+9HD03%K{`R*emaA^ zOBdb=6Sf>w;ajF-``6v~rbT)Gq3C$z9_23K*AJGe&Zz!bGRts-3&==r{dTB;1FXHW zQpnl(j7ILWa*jHCvKdud35F}wE4~%iv*Ex?nb{OsTkXwi81yDkIjjWr%x297puhbn z>=o>*F`Sd?_{@K0{b9DT>_gc2fa%>GAcN5f<=t_H<0OLRP=ebnxMf4#N^32Rwbbvo zr*gSi(|8*^qDbr{NUq98r4_Hf?#6hW#D`yFV+XTVC&;jPLQ=c`z-P~|1KPx|>-Xk) zpKiS;PbMq(1u(SSpPG*vhZ2t+?TivNKacuS_PT}7dhmyeYUp{cOxmVR z?RJzS8^O}Jc{br|yxmqU5{b`PTF4BZ`(hjBc;HqBm!+AqyBL%0i;f-Fwytz26X!t_ zl;J~#h801o4@4rANrL7Fld}c47O9y6i^U|j@G|mms@Tz5Wg_Axg_v2uZ}+v?>`Wjz z820iBzAPKt`JvH~{=>z)7Cs6VlMbH*k9eiYBOk7rXg{y;(JeTz0dRr${;I+!dTz(D zGPXQ@GvEE!rcZq9){~uq-an(wO!8Ywx&u)w%+sM38t3gcDkz0-cWXByme42efwK7a z4B@=eTL)5gEpNVGv8c(GEb!ZcYJj&=5xKhfqrH4y}8}%{L1qj z39%d%L^xhgJI$xFFE7o*iIk4wZmuaQ1I%t%0mW;Nt`GNDclpC>^Ht$&ex1I;Pl99) zeD_O7NhG_lpa+T`v3T&EaU5NEHx-M`OZ0r3VE>uU@c{0qAf4v;k zwBpfNjl~O90~0-SNj4pnyp3sqIb$lKVF~gms%hUGq#%a@dq`Rq5IQ|Gi>cl=MoF^9 z6BkL=N_vU|LOepNU&mgV{D2X)&uHSBb@+srPVg7Vf$nR`MaH?A?k!sY1+&X1J8%Ye zp=R_C?zE$pN(|ym%8C{$!zR+NOARF>-zJdixepqEcC?#wZLHnP^fe@xonh|^tpV}? z?J$&*@{fRm4}72Myuzc@CT7uqZRm=dKstuu8THp=>?skZ%I0|tuoG6Dz)cGkB$S}Q zw%NEE=o0=T4q(FKpe3l7l}cz{Tw~1$+b zMzoZY(DOOhtr$Cw`!_F`)w+=Zv*uR$OaCIAd#)*1Y2&j#y-VoXIVW+9E=bdrk|>`G z2S$gWNikX=wGH?(eOL~Ze~@iR4`Q(=^}}=FSnP3u?N?)M@FbE!g<-{4d1N)%N7F zK02=9`q1Y3Q2dd?pz7x*na57hFXM;+3fZo-%Dn@D_|aAWT;aMAh7N??HC0RAS_|JB zV&PDo{NrIH``YM}r61L-V8|KlKNP=iPbXQ|2r3Gn>hqrQv){z;v*KiPu3j%)im{Um zaayTH_BYe_r7Ri8a7LKPTbP{*_Z>savUIxloncV)`W5`hrinJB`z6y(*Iy3Tklp9c zva`)P$H14)h%XTZ$+-5z@_Xd3?^4^?7V_DiJot9k=L`1XpX8S8*gvz`Mr1D7ztu7o zP{{%$JNlFnv{B3x4p-kL38ln8P`fPcm@WG0FIHm1(~3wczDfc-(VPKg*&-oc$uyr4 zQ8M+_f&FaFij3LfrZr0r&$N=>1K6L*=Xm>yRaB4SEp)ni@&D$&`tqq4UcxF9jzwCAI>9uaji|1v>LQIA+1~8s2M7_{ykhK{1(~|4G>p%2ybW)=82v zpn-VgXOgDXu$Dduf1oe?njJ-Wq@NYq*^zZ&cha&p!_0Y@@DW1!>~t{ksSyyG>XdP| z{maCbNuJ50>~6M(`)w_#DE9MqDf6%jqG(+5Zvnesv`Zq%)qZtX@xsa%^^`*M_#I5Y zDD7DC$!D4Y-RKpbLbNYxpg>}#Fb{*Fo55exP$luV33ye#ejO`MIH^6zxUJC9*YO8I zZ~?e;>UQ@qCUMJvqSNs}MEEC_0arKJ%z6H@`_T3l43dPqeN)cL6R)mTBTP1P&mE}+u6f^vDcNc@=Z_MhH8VtrBvL@2m zsm#oNDdl+fg~kaDf;cPQQKk(jecM69rMHLz zt~2wHv@;*nSxDv^hS9WP^TBQ{drWaG49UORwbvaRH;KB$>izr=#i%qj-R*+DG*7AA zgHp0&GNB?#QL=b6Cg&TrP06_Zd9<<>TVGJm@BdJs_>EKbYf)(oqb|~ZWXB9Of{w~I zcFfA3c;sAaYTF146i${6m9%W}6;V(+I0u#v>5pkbNiD~?+yx7nOSk!fJBpdKKX_;} zIX6jg*iI^hMfzn{tg9Gc-SE>dPK<(R{7nLzWi1Mr+bLWx(h)hTa$|0+Eiz*cd`V;; zaXQs4a;37RSz%_J>096ZD!g^{8|eVMfPL`wq0KOcqC}sl@mN~`(;u5QN9O6hdn@)o zo3wc4^M%hDSk{TM3Ywp zAX`@u+7U$n>uYgjU>pSli&G`fYad65R;oty@q}#`;brT-o98a$Ab1$VtPkdgF(C!p zKo}kbPl5mIkKeKNwMg>b*RRNB$eTC+&yp0ky^fj1cxSL)cx|-v^fdX5IzO~N zkIhlBY@F~P3YGC|0mpDJ^g7|{9+AP7ftq&{ahbX>CMj zPsa}5Hxc1wKn0CUYS-=7!?B_HAMXZ9^R#zUfAyr(m`sg5x%7opP#e##i2FJ{QB)YRqD({XUQt4 z@Qb0pa0}V;RN{7U{{sH%<(7!A6O2OmUG)KiR?U)ZRcnO!&i%3*frCGvc05DylX!dm ztmW_Re<+{y8ry_G0gaFPGmj?qo2~(3xQ>S11H)&(Ey%DD%SKej14X+#>`col@cit#qg%%)I!>bH)!8hCu*&sz=w!vOpectgU$*_8yFu=o z#)=MwqxEV$U3JME_`x%$bZEu#JZ;vuk%l0Y>dZrN^;Zha8xrVf>LaMBZ3vS#gtN&k znf}oHjx$d4=7C+qPHijpVKG3j=Y{xE1}kK2fF1WrlwWf!F5;5i2<(pYor#T+!p+6k z?eavpZgAOPVCIIY8NJxbebi&jBS-{>&19eq(N0MI4@J7g|Hw`xI?~1g6&+7&mDkWP zh@P5>2~Z_SUrtCV!T05dvl_*pf)qyGbah;*@?tQS8i%D0Kpc%l$))_lj93pDVMx#9 zK-F;#OkIQO=@13_0Wt`+W?R#=*{F`69l9B)o+$2tGQGIgOqI~ zuxAk1|_6^MgY@8G)7xqsQ-= zkeR})N$@;FpYkWrj7`E`QDZ3qf4O%-%chO~95EzfM}Q(O)l{zRuMOP#+Ee`?C98=9 zc#z9gOcK>}5GQJf5yOkQ2l#koldvP_?z}aSFcvFsGSF~95SEh9S-bvttj1A)+EmMC z6Yf+#7MA!Z$96KQTS{x8O#!LTw}qA0>|_5R&@(lT0aOU&n@SEhEx{y0s5qv1=)M8x z1PKTy-KUgCF*`y}Tk9{+c(=S9L_{37PLEj^l02klG{4O<)+-x8n+BJcEWdkmq0=@U zdq`D`v9I4kM@Ic7bk*p{m#oY29 zrdD@L7F%i_#fEYC< z>%J?ulf?b)si_QJqGzW+B%ep#^y{zpY~TKS{A4IKoP6yl5LL@V+3Gh-oOW#pOL;6kjhP}ruDndg?E13$yfBE~2{!>h;9J8$0fhRPL z30gLNvxz)vk@d_M1ZbtE<|Ls0LX;z=W=iu%@ zo@v#bC<`fkT|^&v1q+pc+XRWAn{4$q)qGC62ewo(I0-hmulh{2lJ0(6&W=?`UCItT z^ZS(o(DOrNU!)Mj&T9QTo!@MtV~B&Q-Sl!h2?MWGqIn+bg)Cp`3iI=R2Rto2p%Dq{ z7an>P1(Z=Y*%mUFwPk%!;PV6M()-w`O3yN19;AJ!ST-Urm_iNBfbz~lly1U4+Vf>e z%*MN8n7WB?UMoJ9xBmE?i6w1K;Y2G9d+mlHY%*Y^LW7Nwi^7~ zcZuafgs4?%$LS6!?3JHzK`nie&daZ%DuTIiI5xiUUD1tpjrwTTZeKs#*!7zZHRnxw zBQi*xPQ?H`yxl}HW~l?mW(NUAKCZ;qeNP^aCMLJ@H-|{yb}^4bzHfEQx2s(T9e?l$ z5z)$WI)eU5#A!L54&2o|-#2z>vcyjXBIm!jSTEc}49*|XzY4=$AR;1qJ%4_6E#mAX z8*hgvMud1Zvy4=P3TwX~Du9Kx<>R;XFPE}^ILd8ad0u%5Py?c_!A3S57yDFv-R@oa z+IFVh;AuHRu#-D~9Vi7@-#fAOjf?1Kgs8v5HzTV;bJkkjiTgD52$_Q428&=_X7?Wi z&Ha{Sdf7^FD+T8ar{dIt9(F0#F#0OJE$}ZCG6OdXg>xajHL~T8m&+W6gGD=k5*lXz z=8fpG+d;j}nK%TfICsRCbTfIh|ADWVnPa0Z7zPtADD)28-+xw!Qjg3yVGT>(-t8Qm z;B43g?yYlGT&jz$7BYwv(8#UgS#aFpCjCqy(7p$O1*gNMQ==9x@NgpGZ#7vgAeUJfcsWDx@7NtaMx4g?J zEN5L>6mTT|p2okSRZS)Hk~NL2TT?P-&v9{Hqq{Od}Eqs$VXNL5>R z_pp&Nk#eu752nSa@?6y_sXR3JwiH=y!|0qO13}6YyED+6w(YYshs+;8i9}|OExpkXs#)5Zv!l5Y9ZPj@V)$xYYMJX% z1%N=a!nKAQ(DAnUzWhq*n|<2OUt&;E0zK8HbnSlSGpN|=k?LZaiQ}Js79RChDbZyQ zDO++*gtGqOs!<$eoROSib+8cFfFuV-3Cs&j?DneuLj9K=c_1=SPJsUyHB%Hr8Oo06)h~i$hPL4 zZ;P=@zHsMbl)xNRG%oK9ee_eXezlJ=owoFTJ2F!Jg$yNofo-P6p$hOpM5RaU_@q+-3{i1S|2J+FX(<$y6zSnzaW| zOO^9&wb;SkI-1oH?&j{Tqp%Yj?u%GAa_X2eAOO>uQB>~qv<6kX_WOF-&j5G}l~mW3 z=i%deXQ5OXVu;*c?yD?0I*vv_?cHNDQwlx8&hO6eVm}sLenaTd)BD=qlA_1to^s0I zYhVdw#)i7LZ;vvorttCA_1WX667|Ac$K3NEMKu;rFaLZ45C(Z%ynlX68fO6Pr#bu7 zGt4civFN#|=^;IjD^&rzM=@&?G%z8++^MUn@y$|dyt=6!{C#|yKhnYN-7WqGvkGY& zcC~mpaQ~a{Nmu2|K%CQ!;|@vYNnm?kjfZCS91>Dk$w^J9%>^iv$I|4=t3$S#T2n~( zo&WyRbd}~^8ujXeQw)m@A8Hzo0uidTg1Be=u*On_QN5VdZ$#2lUJg+?Tt*Hhs$=)X z%sYzqdIRr!WT17eFmyxhOo6f@l`v+Z7|;%tjWs~^*T*Evdkzt;3%3*qtipm~=yfUJ zZ{yr!|GS=#6)zL?SU(e=_^RIQX(f~{>JHJ(G`d+Z_0zKHHXq}aFQMx2IRM5>O4s0@ zl0kC;xRt3~O{+G>v4cWXO73#pHlJ;yvbg$_Qon>nL)f-i;fHQBP0$B-iIfDQrQ`m- z{a1I6q4MA+{y^oeN6>Q&&#BdoLt;%k&g;g^4$58IQC)N4t@bWQiAEl6#Zcw=Y-Sv$ z`&9Bw4X=K=VS%Nh%SH;PGRB=n_8ndu!lUC_f3_;7CgpXh!%WOs8y;Swu_NKYLJJHL zB2DR}zS;+75jEY$q!uOTT!`+uiX z_^_$%$;t%&Lt#ItQGx7|!)UPka~2%jX#W|sGw7CP0}n*RooM7!(S|fFh^C`74B|xj zys!;^8nl_qb^WFy^OF?Y4)fXg~YY2f?x<#=~LbU$%L*bU+`pJ^tySXVw(bP3T zel3p+M!ePzh%NDmMWxWqjc39U5iUr9=UVpeA1as{xxd@!O(dmhHRK# zKY+d4^x6Nm`%_8%cu>o?$NM+)zhX{f4wQX@*;<9%9#QvvYd9T7Ojo`W?R?m_JHGgm zN0NwGrA4y;{QQkXo)AiM8QbS~XB6`fx&WQ7sW4=$e*SQB`T7jj!;p>d+_L)A`SiqQ zZ*-wsPrK}Bcc&kqZ@j_jIBJ%+@*j$5KLq>Xc zYeTn31L-u`!5_MpUFxZN*rr&FiH^cs3FOa_5X9xvS|!u#gg-0#&OZJH`ulKS{fot} ziB8Z%yvDeD>t!F9<+iz{J0jv|4blwxRmOV5pRQ+aDshnVCSiC#Qza(%AD75M=Bjx0 z+b=(%Qv$!S^^PZ;w)sElRcV(pP(vlkp>HtjSz&tSdL?LA&|}m>py_ZJQ}KxNR#R!N zywF)?kX4w}IgJ4`5w$5h(qXTyJ$Q&59JMX zAbK&?X}qjU&d+BGUUg)b4Ry-Zik8EZ{s}(5dn-JO$C{({T(|vOt)kvAcJ0W!dMO>L z00NST)0ktteKc?)HgPG_N!XaYxE>K5T1w*5E^AD;>Ml{geE3u-m;}33o=@6HPlpgq zFGB`;80PmzsE%pg*U$?e=*1RT)M)a40mTiA`aCb&Jv-M5skZi2caP4($6F)xoZmt~ ztxLhkaY%<$8gV9ihYxQ}e_quJ6l*lW8Fx{-1+)~UXv^HV7ga1@&#P9yZ61v#v!0_> zGB6JeHe*Xo%icJF6N5I{BH{aOMO>^|kpxRYZ=|?{ESy%qWFaCWNYvZKrRjC>Amm~m zxEXWI@8np9jV@C*Y`Oob7LiB1{>+u=Y|hD~(#cI$@`d z>-P8We*gClbhsIlC&?#j7IXR5x32$z%^CE28D6@(9q=tukR^ws414W#e@FETBpxF` z;{2!u!d@8!CTA)m>8(EM@H zAdAn{Fb+w%-98Y#D`IoOSN8Vk9f}D(c?T9Bmic|TZ0C1*xErGlMTdeAV4J-D1QOqs;E<8;>ryH4!MY%Qd&l- zk0nJX4w4&`idc3phMmr7|Qzb$}=UZW;0{#(++S* z`wgE23w~^#WrL$%T$`#I)&5~Qn_nF((t^%ekF6`-8JCS&>YEpu$m?uc8)7IX)Vo0$ z=tFEP6UeHkD^mUenr3YpbZqnp!n%7yu!AMTrq@CJ%!32e3(Eu*VIrdx-su|Z074b$ zfZvuaVQYVACKlssobCl@Na@D9w1F{x$$?tfR2PIiF-{TYM2v-~S1r(Ae_q2n1J zQ2RvwCJCSzl1KM752mwF?~vl9)D)&almddpzTiUySY@3$)2@~+S(s_9#X(y7ZXNMy< z$IBN=vN?i-NIxpMj>4XK?{prnMR4}R(<5PBsQn8s`3C&mHU2)AB97}1FaKcjLR9I- z@w1o0?Z#>N+HUm5e_GNaaQH{MPrZm+tf(dz?EJLV@^+{EI&p-So;tlv>TTB*zTYCI zW{=Nz1i~6rZ_Z$CNAidhgu2tpMHRnC+sfKgE!@uYQb>PrfwiX>*%3E35yaxp&d1`v zH+d7`cmFB?anWaXAau8Uf%u1d_)jK>0vXBk0^J|qcu+XaT(l)RZy%@`|6Tiz)R|5O z{(N}qrdZGiXQv=wd>1V{?!8~IlDv%ld#EU0Jnft=cUalZpG7ChskFkneV_e>+%AQ0 z`!q|}3?_Qm^%uOou6|qOiXTx4vkj_DJibU78}W-jvgw)ndE5_AC3}q=F{KM@ zAlz;1;cTlXMie48-ajs5on(J`0_~a|KIY?9@X_Bq^Wzu=Ke;^?T=G&?Kb-|_>oV^L z^tHCFP_*k`Au@L(DO#sqdMB$xFJE-UQC)|vFx%BQUIoPcz8p)R{O9Qyt8!dA-+5y? zw}qmH^&0=?47c$er3!NB=;Pc}!pa@1g-qy9XH(;Ef#B%ke_64n{!)FogNWkl=fL9u zq{o+RnR3a@y5&A+#Jz;tBT3=Y-xw0q#c0I2X%>#2iZzb0s~F@wKI3$9{BV8l06pNA zo^(S5B%gMQJgIy7h#UD|yVRJr;L4S={D&g@E&2&|GL)d4QoVvmL&7B3HRZA(O^KIu z6cI}fA%Ky^M>bm{gG}B;!GbLXjf2@mwZ8J1MpYey@6^C`<+zou_W-)0oF_C)kyEy@ zGWmqC<`U}7SnN1!xBx?;#2}dm&BUDXN7R7eY1RGWvxX;DB_UMhrYAN(>G*2-` zoJ@12)1fh;Qu=O+MXP$ZpF+}YZ9g!(G3wB_JZCIP58K`{=^jwQI3d{%Fk&wU`s70W z8j`nxBHOcO!L=R>D-q2&mQ~fuVh!3|H%60KzqwaU;NBq zxCWqI#PIz^c3NXB?<;*Z<-)qzaC$lY`jvC|l)x9B{ZjWyQrEma&5Lf&VUn`1=2hEi z6;&D6%E*3fwVoly*XaXgJSF^n9#4E~W%K0KVekgKr(?YB0eQ!nV?B`=iZB}0j4vvO zxbHu)XrcNL!yJ(U`@>0{U{jk@`>MK|;zBH$IJ|da-(7Se7I6j;aileH-?Ajr>kGhk zj>&Ji;oJL?xy|NU;M*%yV%**0uQyZCY*1&Bm<$Q{{%1T+sF+O* ze)IcBvZMC)4LPouwkq<`Kh(Dz#&AzR3BS+H;Wv1C=|a|FzoeI?ANdOVc2O0Togu5N zDEY_XI-7R)es_2G{u!-wSbx^*E*JcBafEp-^637CBhM#C9%s{IkrkeVlga z)P_kkS*aNXG8*aTP{}dszFr(r66kdUfJ|F= z%3%0Obu27(1F}O|HhClm94!iX=uwjNwtB;Yt&)%RqSyS|AI7MBv z{b)-}(`_N{)ZWj|L|g1G%M2rq5VSr-P-{m>2bj`IBJ7+clq+pu$KH$V#d~lNJ0DQc zYK=RjkQmhPFfJ%H^zX?)W&$C|y!U1*M#?a>v&_)FahR$63^+6K`(GMBtbAFRmgff= zrN_MvrJtB#Z8zssqapDM_ce4>QYv)`CRsrEAF@&-n5b-nE_^5XPk7TtD4-pJbI3Q+ zI&pEaa&f-Aes4;-L?}HmJ}2|e5I6UuK5_S6WN5>NoO42Yifl6g%t|@I6r7#BSI)HR zqK^(0ta)eYGIq>(Cku4@2|-yO6teok@jNA_(k*>wx)C`=S9i9&*|K}ZZy&vSeCSy7 z;s?4o8yF$!c-4Y@9SgTCz5ICCU`so$9Wsuud6B~tQrLCi6(t2v{!(b6)NzXayE%#w z%YSOV%Q3*!Z6nirxIOYPUs?6oytr*(}9dWJ|vc>(s?Gm6+-!~rE0>%!kkvg zQrWVM=hYBVBuKxY-j`P`Su^<}M|O*gtV&CgS<9V?T5!3R`qLRue(m*Jmo(toMFIn} ze)Zn@0LfCCgFqvH04!?9SEi;JsVW`EnmT%rA{|Gqq=6hz&WKW=8?hH z$^m;u&uXzzu{d-G^HJwR#r#%X3sm6C7fp4mot1OLb?3yVS~d;cWT925V}&ZUkBr>C z$41p^vHzh|8e4Qvkm@24dlGJ&joon?a|***juMv1Sc?qlEF{6@2kGoR*{STn5xlk8 zbB0yS@mR|<)+g0HZmW0)j2$lYc-(+b3>w9`;;(Mii_(uiQ>0mNt3%N$$8G2I`sCdl zas1gt^ee%?-Wp;AtR~W_t1Q|mp#Ml#U}AoJ>U1i^($BwrLF6$*h_=uk?@b%j-$m_S z^#f7}e_|tyggs5w^gFwyF2&NX2k47m5)T4;_M?L+zidCBYmx^8y?tL!<3{XlO#I=! zYv#^pROB|y8oQ2IoWD>6F#QJ91BG8+?vJB`P%~a5aJn;=KB~=2l#2@H8(HZB|* zqF*r!hl`$3(cDg*ENHIgm0dsA-v)_#E|7e=SIhkT9|{Io=U00I=$iN0&(nnO=kD#T zF9jkW{iPSUj9Q__~+jHRQ=D-w=O>L-YiOi z0_T^BjifKz<>TX>S(M|ir~gm_j$@VH25|HuH4*(ryCkT9fZ8oks3)uNdqM&#jA#G* z7e7w<0%GfvUomFRSP)4A5^Efu*vI$aQabI;QRJqtArDV>`--Hk?

W|T4iJl|A)ig~ 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 +#include +#include + +#include +#include + +void authcallback(int err, wchar_t *reason, void *data) +{ + printf("Logged in: %i\n", err); +} + +int main(int argc, char **argv) +{ + int i; + struct pollfd pfd; + int fd, done; + struct dc_response *resp; + struct dc_intresp *ires; + + dc_init(); + fd = dc_connect("localhost", -1); + done = 0; + while(!done) + { + pfd.fd = fd; + pfd.events = POLLIN; + if(dc_wantwrite()) + pfd.events = POLLOUT; + if(poll(&pfd, 1, -1) < 0) + { + perror("poll"); + exit(1); + } + if((pfd.revents & POLLIN) && dc_handleread()) + done = 1; + if((pfd.revents & POLLOUT) && dc_handlewrite()) + done = 1; + while((resp = dc_getresp()) != NULL) + { + if(resp->cmdname == NULL) + { + printf("Connected\n"); + dc_loginasync(NULL, 0, NULL, authcallback, NULL); + } + dc_freeresp(resp); + } + } + dc_cleanup(); + return(0); +} diff --git a/config/CVS/Entries b/config/CVS/Entries new file mode 100644 index 0000000..f757bc9 --- /dev/null +++ b/config/CVS/Entries @@ -0,0 +1,7 @@ +/Makefile.am/1.5/Sun Dec 19 02:58:01 2004// +/dc-filter/1.2/Wed Dec 15 18:59:51 2004// +/dc-filtercmd/1.1/Wed Dec 15 19:01:20 2004// +/doldacond.conf/1.1/Sat Sep 18 14:53:07 2004// +/locktouch.c/1.1/Sat Sep 18 03:36:41 2004// +/speedrec.c/1.2/Fri Sep 24 09:48:38 2004// +D diff --git a/config/CVS/Repository b/config/CVS/Repository new file mode 100644 index 0000000..65bc6a8 --- /dev/null +++ b/config/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/config diff --git a/config/CVS/Root b/config/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/config/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/config/Makefile.am b/config/Makefile.am new file mode 100644 index 0000000..56c357c --- /dev/null +++ b/config/Makefile.am @@ -0,0 +1,21 @@ +bin_PROGRAMS=locktouch +libexec_PROGRAMS=speedrec + +speedrec_SOURCES=speedrec.c + +locktouch_SOURCES=locktouch.c + +EXTRA_DIST=doldacond.conf dc-filter dc-filtercmd + +install-data-local: + $(mkinstalldirs) $(DESTDIR)$(sysconfdir); \ + for file in doldacond.conf; do \ + if [ ! -e $(DESTDIR)$(sysconfdir)/$$file ]; then \ + $(INSTALL_DATA) $(srcdir)/$$file $(DESTDIR)$(sysconfdir)/$$file; \ + fi; \ + done; \ + for file in dc-filter dc-filtercmd; do \ + if [ ! -e $(DESTDIR)$(sysconfdir)/$$file ]; then \ + $(INSTALL) $(srcdir)/$$file $(DESTDIR)$(sysconfdir)/$$file; \ + fi; \ + done diff --git a/config/dc-filter b/config/dc-filter new file mode 100644 index 0000000..0b31bbf --- /dev/null +++ b/config/dc-filter @@ -0,0 +1,99 @@ +#!/bin/sh +trap "" SIGHUP SIGPIPE +if [ ! -d $HOME/dc ]; then mkdir $HOME/dc; fi +if [ ! -d $HOME/dc/done ]; then mkdir $HOME/dc/done; fi +if [ ! -d $HOME/dc/resume ]; then mkdir $HOME/dc/resume; fi +if [ ! -d $HOME/dc/users ]; then mkdir $HOME/dc/users; fi +exec 2>>$HOME/dc/filterlog +cd $HOME/dc +unset speedrec +for dir in /usr/libexec /usr/local/libexec; do + if [ -x "${dir}/speedrec" ]; then + speedrec="${dir}/speedrec" + fi +done +if [ -z "$speedrec" ]; then + echo "could not find speedrec - using cat instead" >&2 +fi +maxsize=0 +unset resfile +unset infofile +found=y +while [ -z "$resfile" -a "$found" = y ]; do + unset found + for file in resume/*.info; do + if [ ! -r "$file" ]; then continue; fi + . "$file" + if [ "$filesize" -eq "$2" ]; then + thisfile="${file%.info}" + if [ ! -e "${thisfile}.lock" ]; then + size="$(wc -c <"$thisfile")" + found=y + if [ "$size" -gt "$maxsize" ]; then + maxsize="$size" + resfile="$thisfile" + infofile="$file" + fi + fi + fi + done + if [ -n "$resfile" ]; then + if ! locktouch "${resfile}.lock"; then + unset resfile + fi + fi +done +unset "${!dcarg_@}" +if [ -n "$resfile" ]; then + . "${resfile}.info" +fi +origname="$1" +shift +filesize="$1" +shift +peername="$1" +shift +while [ $# -gt 1 ]; do + rec="$1" + shift + val="$1" + shift + declare "dcarg_$rec"="$val" +done +if [ -z "$resfile" ]; then + resfile="$(mktemp resume/resXXXXXX)" + chmod 644 "$resfile" + maxsize=0 + >"${resfile}.lock" +fi +declare -p origname filesize "${!dcarg_@}" >"${resfile}.info" +echo "resume $maxsize" +if [ -z "$speedrec" ]; then + cat >>"$resfile" +else + peerfile="$(tr / _ <<<"$peername")" + "$speedrec" "$HOME/dc/users/$peerfile" >>"$resfile" +fi +size="$(wc -c <"$resfile")" +if [ "$size" -eq 0 ]; then + rm -f "$resfile" "${resfile}.info" "${resfile}.lock" + exit 1 +fi +rm -f "${resfile}.lock" +if [ "$size" -lt "$filesize" ]; then + exit 1 # Exit code != 0 means restart transfer +fi +rm -f "${resfile}.info" + +destdir="$HOME/dc/done" + +newname="$destdir"/"$origname" +if [ -e "$newname" ]; then + newname="$(mktemp "${newname}XXXXXX")" +fi +mv "$resfile" "$newname" +if [ -x $HOME/dc/complete ]; then + export "${!dcarg_@}" + exec $HOME/dc/complete "$newname" +fi +exit 0 diff --git a/config/dc-filtercmd b/config/dc-filtercmd new file mode 100755 index 0000000..96444eb --- /dev/null +++ b/config/dc-filtercmd @@ -0,0 +1,42 @@ +#!/bin/sh +cmd="$1" +shift + +if [ "$cmd" = userspeeda ]; then + while [ $# -gt 0 ]; do + peerfile="$(tr / _ <<<"$1")" + if [ -r "$HOME/dc/users/$peerfile" ]; then + speed="$((read num; read max; read nent; avg=0; for i in $(seq 1 $nent); do read ent; let avg+=$ent; done; let avg/=$nent; echo $avg) < "$HOME/dc/users/$peerfile")" + echo $speed + else + echo -1 + fi + shift + done +fi + +if [ "$cmd" = userspeedm ]; then + while [ $# -gt 0 ]; do + peerfile="$(tr / _ <<<"$1")" + if [ -r "$HOME/dc/users/$peerfile" ]; then + speed="$((read num; read max; echo $max) < "$HOME/dc/users/$peerfile")" + echo $speed + else + echo -1 + fi + shift + done +fi + +if [ "$cmd" = rmtag ]; then + for f in "$HOME"/dc/resume/*.info; do + if [ ! -e "${f%.info}.lock" ]; then + unset dcarg_tag + . "$f" + if [ "$dcarg_tag" = "$1" ]; then + rm "$f" + rm "${f%.info}" + fi + fi + done +fi diff --git a/config/doldacond.conf b/config/doldacond.conf new file mode 100644 index 0000000..dd7b332 --- /dev/null +++ b/config/doldacond.conf @@ -0,0 +1,56 @@ +# Dolda Connect default configuration file + +# Default nick name +set cli.defnick DoldaConnectUser + +# Net mode: +# 0 - Active +# 1 - Passive +# 2 - Will be SOCKS proxy when implemented +set net.mode 0 + +# If 1, then accept UI connections only over the loopback interface +set ui.onlylocal 0 + +# If 1, then enable authenticationless login (don't use without turning on ui.onlylocal, unless you know what you're doing) +set auth.authless 0 + +set transfer.slots 6 + + +# Settings specific to the Direct Connect network: + +# Valid strings are: +# 56Kbps +# Satellite +# DSL +# Cable +# LAN(T1) +# LAN(T3) +set dc.speedstring LAN(T1) +# Description string: +set dc.desc Dolda Connect User + +# Shares: "share sharename sharepath", where sharename is the name of the share as the peers see it, and sharepath is the local path to the shared files +share Video /home/pub/divx +share Music /home/pub/MP3 + +# Allowed users and privileges +# Syntax is "user username [-]privs..." +# username can be default to match any user that doesn't match the other entries +# privs can be any of the following: +# disallow - The only negative permission, which disallows a user from logging in +# admin - Involves commands controlling the function of the daemon, such as shutting it down remotely +# fnetctl - Allows connecting and disconnecting fnetnodes (or "Hubs") +# trans - Allows queuing of transfers +# transcu - Allows cancelling of uploads +# chat - Allows sending and receiving of chat messages +# srch - Allows submitting of search requests +# all - Equivalent of specifying all the above permissions, including disallow +# A minus sign preceding a privilege specification revokes that privilege (or, for "all", revokes all privileges, _including_ "disallow"). +# The privileges are scanned from left to right, so "all -disallow" is not the same as "-disallow all". + +# In this default configuration, the user called "youruser" (that is, change it), +# has all privileges except, of course, "disallow", and all other users are disallowed from logging in. +user default disallow +user youruser all -disallow diff --git a/config/locktouch.c b/config/locktouch.c new file mode 100644 index 0000000..da7b862 --- /dev/null +++ b/config/locktouch.c @@ -0,0 +1,27 @@ +#include +#include +#include +#include +#include + +int main(int argc, char **argv) +{ + int fd; + + if(argc < 2) + { + fprintf(stderr, "usage: locktouch lockfile\n"); + exit(1); + } + if((fd = open(argv[1], O_CREAT | O_EXCL, 0666)) < 0) + { + if(errno != EEXIST) + { + perror(argv[1]); + exit(2); + } + exit(1); + } + close(fd); + return(0); +} diff --git a/config/speedrec.c b/config/speedrec.c new file mode 100644 index 0000000..be0ed2d --- /dev/null +++ b/config/speedrec.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +char buf[4096]; +volatile int eof; + +void sighandler(int sig) +{ + eof = 1; +} + +int main(int argc, char **argv) +{ + int i; + int ret, fd; + time_t starttime, endtime; + long long numbytes; + size_t datalen; + struct pollfd pfd[2]; + FILE *recfile; + int thisrec, numrecs, numuses, maxrec; + int recs[5]; + + if(argc < 2) + { + fprintf(stderr, "usage: speedrec recfile\n"); + exit(1); + } + numbytes = 0; + starttime = endtime = 0; + datalen = 0; + eof = 0; + signal(SIGHUP, sighandler); + signal(SIGINT, sighandler); + signal(SIGTERM, sighandler); + while(1) + { + pfd[0].fd = 0; + if(eof || (datalen >= sizeof(buf))) + pfd[0].events = 0; + else + pfd[0].events = POLLIN; + pfd[1].fd = 1; + if(datalen > 0) + pfd[1].events = POLLOUT; + else + pfd[1].events = 0; + pfd[0].revents = pfd[1].revents = 0; + ret = poll(pfd, 2, -1); + if((ret < 0) && (errno != EINTR)) + { + perror("cannot poll"); + exit(1); + } + if(pfd[0].revents & (POLLIN | POLLERR | POLLHUP | POLLNVAL)) + { + ret = read(0, buf + datalen, sizeof(buf) - datalen); + if((ret < 0) && (errno != EINTR)) + { + perror("cannot read"); + exit(1); + } + if(ret == 0) + eof = 1; + if(ret > 0) + { + datalen += ret; + if(starttime == 0) + starttime = time(NULL); + endtime = time(NULL); + } + numbytes += ret; + } + if(pfd[1].revents & (POLLOUT | POLLERR | POLLHUP | POLLNVAL)) + { + ret = write(1, buf, datalen); + if((ret < 0) && (errno != EINTR)) + { + perror("cannot write"); + exit(1); + } + memmove(buf, buf + ret, datalen -= ret); + } + if(eof && (datalen == 0)) + break; + } + if((starttime == 0) || (endtime == 0) || (endtime == starttime)) + exit(0); + if(numbytes == 0) + exit(0); + thisrec = (int)(numbytes / ((long long)(endtime - starttime))); + if((fd = open(argv[1], O_RDWR | O_CREAT, 0666)) < 0) + { + perror(argv[1]); + exit(1); + } + recfile = fdopen(fd, "r+"); + close(0); + close(1); + flock(fd, LOCK_EX); + if(fscanf(recfile, "%i\n", &numuses) < 1) + numuses = 0; + if(fscanf(recfile, "%i\n", &maxrec) < 1) + maxrec = 0; + if(fscanf(recfile, "%i\n", &numrecs) < 1) + numrecs = 0; + for(i = 0; i < numrecs; i++) + fscanf(recfile, "%i\n", &recs[i]); + if(numrecs == 5) + { + for(i = 0; i < 4; i++) + recs[i] = recs[i + 1]; + numrecs = 4; + } + recs[numrecs++] = thisrec; + rewind(recfile); + ftruncate(fd, 0); + fprintf(recfile, "%i\n", numuses + 1); + fprintf(recfile, "%i\n", (thisrec > maxrec)?thisrec:maxrec); + fprintf(recfile, "%i\n", numrecs); + for(i = 0; i < numrecs; i++) + fprintf(recfile, "%i\n", recs[i]); + flock(fd, LOCK_UN); + fclose(recfile); + return(0); +} diff --git a/configure.in b/configure.in new file mode 100644 index 0000000..139635b --- /dev/null +++ b/configure.in @@ -0,0 +1,213 @@ +AC_INIT(daemon/main.c) +AM_INIT_AUTOMAKE([doldaconnect], [0.1]) +AM_CONFIG_HEADER(config.h) + +AC_PROG_CC +AC_PROG_INSTALL + +AM_GNU_GETTEXT_VERSION(0.12.1) +AM_GNU_GETTEXT([external]) +AM_PROG_LIBTOOL + +AC_CHECK_LIB(pam, pam_start, , AC_MSG_ERROR([*** must have PAM])) +AC_CHECK_LIB(dl, dlopen, , AC_MSG_ERROR([*** PAM requires a working dynamic loader (libdl)])) +AC_CHECK_LIB(z, deflate, , AC_MSG_ERROR([*** must have zlib])) +AC_CHECK_LIB(bz2, BZ2_bzWriteOpen, , AC_MSG_ERROR([*** must have bzlib])) + +clients= +extlibs= +experimental=no + +AC_CHECK_PROG([PKG_CONFIG], pkg-config, yes, no) + +AC_ARG_WITH(gtk2, [ --with-gtk2 Enable GTK2 support]) +if test "$with_gtk2" = no; then + HAS_GTK2=no +fi +if test "$HAS_GTK2" != no; then + if test "$PKG_CONFIG" = yes; then + AC_MSG_CHECKING([for GTK2 package information]) + if pkg-config --modversion gtk+-2.0 >/dev/null 2>&1; then + AC_MSG_RESULT(yes) + else + AC_MSG_RESULT(no) + HAS_GTK2=no + fi + if test "$HAS_GTK2" != no; then + AC_CHECK_LIB(gtk-x11-2.0, gtk_init, [HAS_GTK2=yes], [HAS_GTK2=no], `pkg-config --libs gtk+-2.0`) + fi + if test "$HAS_GTK2" = yes; then + cpp_bak="$CPPFLAGS" + CPPFLAGS="$CPPFLAGS `pkg-config --cflags gtk+-2.0`" + AC_CHECK_HEADER(gtk/gtk.h, [], [HAS_GTK2=no]) + CPPFLAGS="$cpp_bak" + fi + else + HAS_GTK2=no + fi +fi +if test "$with_gtk2" = yes -a "$HAS_GTK2" = no; then + AC_MSG_ERROR([*** cannot find GTK2 on this system]) +fi + +gtk2ui_msg=No +AC_ARG_ENABLE(gtk2ui, [ --enable-gtk2ui Enable the GTK2 user interface]) +if test "$enable_gtk2ui" = yes -a "$HAS_GTK2" = no; then + AC_MSG_ERROR([*** cannot build the GTK2 UI without a GTK2 library]) +fi +if test "$enable_gtk2ui" != no -a "$HAS_GTK2" = yes; then + clients="$clients gtk2" + gtk2ui_msg=Yes +fi + +gtk2pbar_msg=No +AH_TEMPLATE(ENABLE_GTK2PBAR, [define to compile GTK2 progress bars (experimental)]) +AC_ARG_ENABLE(gtk2pbar, [ --enable-gtk2pbar Enable GTK2 progress bars (experimental)]) +if test "$enable_gtk2pbar" = yes; then + if test "$HAS_GTK2" = no; then + AC_MSG_ERROR([*** cannot build GTK2 progress bars without GTK2]) + fi + experimental=yes + gtk2pbar_msg=Yes + AC_DEFINE(ENABLE_GTK2PBAR) +fi + +gnometrapplet_msg=No +AC_ARG_ENABLE(gnomeapplet, [ --enable-gnomeapplet Enable GNOME transfer applet (experimental)]) +if test "$enable_gnomeapplet" = yes; then + experimental=yes + gnometrapplet_msg=Yes + clients="$clients gnome-trans-applet" +fi + +guile_msg=No +if test "$with_guile" = yes; then + GUILE_FLAGS + extlibs="$extlibs guile" + guile_msg=Yes +fi + +krb_msg=No +AH_TEMPLATE(HAVE_KRB5, [define to compile support for Kerberos 5 (not GSS-API) authentication]) +AC_ARG_WITH(krb5, [ --with-krb5[=PATH] Enable Kerberos 5 (not GSSAPI) authentication]) +if test "$with_krb5" != no; then + cpp_bak="$CPPFLAGS" + ld_bak="$LDFLAGS" + if test "$with_krb5" != yes; then + CPPFLAGS="$CPPFLAGS -I${with_krb5}/include" + LDFLAGS="$LDFLAGS -L${with_krb5}/lib" + fi + AC_CHECK_LIB(krb5, krb5_init_context, [HAS_KRB5=yes], [HAS_KRB5=no]) + if test "$HAS_KRB5" = yes; then + AC_CHECK_HEADER(com_err.h, [HAS_COMERR=yes], [HAS_COMERR=no]) + if test "$HAS_COMERR" = no; then + AC_CHECK_HEADER(et/com_err.h, [HAS_COMERR=yes; CPPFLAGS="$CPPFLAGS -I/usr/include/et"], []) + fi + fi + if test "$HAS_COMERR" = no; then + HAS_KRB5=no + fi + if test "$HAS_KRB5" = no; then + CPPFLAGS="$cpp_bak" + LDFLAGS="$ld_bak" + fi +fi +KRB5_LDADD= +if test -n "$with_krb5" -a "$with_krb5" != no -a "$HAS_KRB5" != yes; then + AC_MSG_ERROR([*** cannot find Kerberos 5 on this system - try passing --with-krb5=/path/to/kerberos]) +fi +if test "$with_krb5" != no -a "$HAS_KRB5" = yes; then + AC_DEFINE(HAVE_KRB5) + KRB5_LDADD=-lkrb5 + krb_msg=Yes +fi +AC_SUBST([KRB5_LDADD]) + +AC_CHECK_FUNC(vswprintf, , AC_MSG_ERROR([*** must have vswprintf])) +AH_TEMPLATE(HAVE_WCSCASECMP, [define if your system implements wcscasecmp]) +AC_CHECK_FUNC(wcscasecmp, [ AC_DEFINE(HAVE_WCSCASECMP) ]) + +AH_TEMPLATE(HAVE_LINUX_SOCKIOS_H, [define if you have linux/sockios.h on your system]) +AC_CHECK_HEADER([linux/sockios.h], [ AC_DEFINE(HAVE_LINUX_SOCKIOS_H) ]) + +AH_TEMPLATE(HAVE_IPV6, [define if your system supports IPv6 and you wish to compile with support for it]) +AC_CHECK_MEMBER(struct sockaddr_in6.sin6_family, [ AC_DEFINE(HAVE_IPV6) ], , [#include ]) + +AH_TEMPLATE(HAVE_RESOLVER, [define if your system supports the res_* functions to fetch DNS RRs]) +AC_CHECK_LIB(resolv, res_query, [ AC_DEFINE(HAVE_RESOLVER) + LDFLAGS="$LDFLAGS -lresolv" ]) + +AC_HEADER_STDC +AC_HEADER_DIRENT +AC_HEADER_SYS_WAIT + +AC_TYPE_PID_T +AC_TYPE_SIZE_T +AC_HEADER_TIME +AC_TYPE_SIGNAL + +CPPFLAGS="-I\$(top_srcdir)/include $CPPFLAGS" + +AC_SUBST([clients extlibs]) +AC_OUTPUT([ +Makefile +daemon/Makefile +lib/Makefile +lib/guile/Makefile +lib/guile/dolcon/Makefile +clients/Makefile +clients/gtk2/Makefile +clients/gnome-trans-applet/Makefile +include/Makefile +po/Makefile.in +config/Makefile +autopackage/dolcon.apspec +]) + +echo +echo "Dolda Connect has been configured with the following settings:" +echo +echo " Kerberos 5 support: $krb_msg" +echo " GTK2 user interface: $gtk2ui_msg" +echo " GTK2 progress bars: $gtk2pbar_msg" +echo " Guile extension library: $guile_msg" +echo " GNOME transfer applet: $gnometrapplet_msg" +echo + +if tput bold >/dev/null 2>&1 && tty <&2 >/dev/null 2>&1; then + hastput=y +fi +if test "$HAS_GTK2" = no -a "$with_gtk2" != no -a "$enable_gtk2ui" != no; then + if test "$hastput" = y; then + tput bold + tput setf 4 2>/dev/null + fi + echo -n " Warning: " >&2 + if test "$hastput" = y; then + tput sgr0 + fi + echo "Could not find a GTK2 development installation on this system." >&2 + echo " That means you won't get a UI." >&2 + echo " Make absolutely sure this is what you want!" >&2 + if test "$hastput" = y; then + tput bel + fi + sleep 1 +fi +if test "$experimental" = yes; then + if test "$hastput" = y; then + tput bold + tput setf 4 2>/dev/null + fi + echo -n " Warning: " >&2 + if test "$hastput" = y; then + tput sgr0 + fi + echo "You have enabled one or more experimental features!" >&2 + echo " Please don't complain that it doesn't work, unless" >&2 + echo " you have something constructive to add about the situation." >&2 + if test "$hastput" = y; then + tput bel + fi + sleep 1 +fi diff --git a/daemon/CVS/Entries b/daemon/CVS/Entries new file mode 100644 index 0000000..186e6b2 --- /dev/null +++ b/daemon/CVS/Entries @@ -0,0 +1,31 @@ +/Makefile.am/1.8/Fri Oct 14 14:34:30 2005// +/auth-krb5.c/1.10/Thu Oct 13 03:07:59 2005// +/auth-pam.c/1.1.1.1/Tue May 11 15:47:16 2004// +/auth.c/1.2/Fri Aug 6 18:51:28 2004// +/auth.h/1.2/Fri Aug 6 18:51:45 2004// +/client.c/1.7/Mon Oct 3 03:13:14 2005// +/client.h/1.5/Mon Oct 3 03:13:19 2005// +/conf.c/1.3/Fri Aug 13 11:37:34 2004// +/conf.h/1.2/Tue Jul 27 23:49:40 2004// +/emacs-local/1.1.1.1/Tue May 11 15:47:05 2004// +/filenet.c/1.9/Wed Jul 13 00:08:53 2005// +/filenet.h/1.7/Tue Oct 26 02:18:27 2004// +/fnet-dc.c/1.49/Thu Oct 13 15:39:39 2005// +/log.c/1.2/Thu Sep 30 01:19:08 2004// +/log.h/1.2/Tue Aug 9 11:06:22 2005// +/main.c/1.10/Wed Oct 12 20:33:22 2005// +/module.h/1.1.1.1/Tue May 11 15:47:31 2004// +/net.c/1.12/Wed Aug 10 12:38:38 2005// +/net.h/1.6/Wed Sep 22 02:25:29 2004// +/search.c/1.9/Thu Oct 13 03:08:31 2005// +/search.h/1.4/Tue Oct 26 02:22:10 2004// +/sysevents.h/1.3/Thu Sep 30 01:20:23 2004// +/tiger.c/1.3/Wed Sep 28 19:57:08 2005// +/tiger.h/1.3/Wed Sep 28 19:57:16 2005// +/transfer.c/1.18/Thu Oct 13 03:08:42 2005// +/transfer.h/1.9/Sun Oct 9 02:50:12 2005// +/ui.c/1.19/Fri Oct 14 23:21:24 2005// +/uiretref/1.6/Tue Aug 3 16:56:12 2004// +/utils.c/1.15/Fri Oct 14 23:20:43 2005// +/utils.h/1.15/Fri Oct 14 23:20:49 2005// +D diff --git a/daemon/CVS/Repository b/daemon/CVS/Repository new file mode 100644 index 0000000..b88236b --- /dev/null +++ b/daemon/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/daemon diff --git a/daemon/CVS/Root b/daemon/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/daemon/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/daemon/Makefile.am b/daemon/Makefile.am new file mode 100644 index 0000000..342ad7a --- /dev/null +++ b/daemon/Makefile.am @@ -0,0 +1,30 @@ +bin_PROGRAMS=doldacond +doldacond_SOURCES= main.c \ + search.c \ + search.h \ + transfer.c \ + transfer.h \ + sysevents.h \ + module.h \ + filenet.c \ + filenet.h \ + fnet-dc.c \ + auth.c \ + auth.h \ + auth-pam.c \ + auth-krb5.c \ + client.c \ + client.h \ + net.c \ + net.h \ + utils.c \ + utils.h \ + log.c \ + log.h \ + ui.c \ + conf.c \ + conf.h \ + tiger.c \ + tiger.h +EXTRA_DIST=emacs-local +doldacond_LDADD=@KRB5_LDADD@ -lbz2 -lz diff --git a/daemon/auth-krb5.c b/daemon/auth-krb5.c new file mode 100644 index 0000000..62827c6 --- /dev/null +++ b/daemon/auth-krb5.c @@ -0,0 +1,590 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "auth.h" +#include "utils.h" +#include "conf.h" +#include "log.h" +#include "module.h" +#include "sysevents.h" + +#ifdef HAVE_KRB5 + +#include +#include + +struct krb5data +{ + int state; + krb5_auth_context context; + krb5_ticket *ticket; + krb5_creds *creds; + krb5_ccache ccache; + int renew; + struct timer *renewtimer; + char *username, *cname; +}; + +static void setrenew(struct krb5data *data); + +static krb5_context k5context; +static krb5_principal myprinc; +static krb5_keytab keytab; + +static void releasekrb5(struct krb5data *data) +{ + if(data->renewtimer != NULL) + canceltimer(data->renewtimer); + if(data->context != NULL) + krb5_auth_con_free(k5context, data->context); + if(data->ticket != NULL) + krb5_free_ticket(k5context, data->ticket); + if(data->creds != NULL) + krb5_free_creds(k5context, data->creds); + if(data->username != NULL) + free(data->username); + if(data->cname != NULL) + free(data->cname); + free(data); +} + +static void release(struct authhandle *auth) +{ + releasekrb5((struct krb5data *)auth->mechdata); +} + +static struct krb5data *newkrb5data(void) +{ + struct krb5data *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + return(new); +} + +static int inithandle(struct authhandle *auth, char *username) +{ + int ret; + struct krb5data *data; + + data = newkrb5data(); + if((ret = krb5_auth_con_init(k5context, &data->context)) != 0) + { + flog(LOG_ERR, "could initialize Kerberos auth context: %s", error_message(ret)); + releasekrb5(data); + return(1); + } + krb5_auth_con_setflags(k5context, data->context, KRB5_AUTH_CONTEXT_DO_SEQUENCE); + data->username = sstrdup(username); + data->state = 0; + auth->mechdata = data; + return(0); +} + +/* Copied from MIT Kerberos 5 1.3.3*/ +static krb5_boolean my_krb5_kuserok(krb5_context context, krb5_principal principal, const char *luser, const char *loginfile, int authbydef) +{ + struct stat sbuf; + struct passwd *pwd; + char pbuf[MAXPATHLEN]; + krb5_boolean isok = FALSE; + FILE *fp; + char kuser[65]; + char *princname; + char linebuf[BUFSIZ]; + char *newline; + int gobble; + + /* no account => no access */ + if ((pwd = getpwnam(luser)) == NULL) { + return(FALSE); + } + (void) strncpy(pbuf, pwd->pw_dir, sizeof(pbuf) - 1); + pbuf[sizeof(pbuf) - 1] = '\0'; + (void) strncat(pbuf, loginfile, sizeof(pbuf) - 1 - strlen(pbuf)); + + if (access(pbuf, F_OK)) { /* not accessible */ + /* + * if he's trying to log in as himself, and there is no .k5login file, + * let him. To find out, call + * krb5_aname_to_localname to convert the principal to a name + * which we can string compare. + */ + if (authbydef) { + if (!(krb5_aname_to_localname(context, principal, + sizeof(kuser), kuser)) + && (strcmp(kuser, luser) == 0)) { + return(TRUE); + } + } else { + return(FALSE); + } + } + if (krb5_unparse_name(context, principal, &princname)) + return(FALSE); /* no hope of matching */ + + /* open ~/.k5login */ + if ((fp = fopen(pbuf, "r")) == NULL) { + free(princname); + return(FALSE); + } + /* + * For security reasons, the .k5login file must be owned either by + * the user himself, or by root. Otherwise, don't grant access. + */ + if (fstat(fileno(fp), &sbuf)) { + fclose(fp); + free(princname); + return(FALSE); + } + if ((sbuf.st_uid != pwd->pw_uid) && sbuf.st_uid) { + fclose(fp); + free(princname); + return(FALSE); + } + + /* check each line */ + while (!isok && (fgets(linebuf, BUFSIZ, fp) != NULL)) { + /* null-terminate the input string */ + linebuf[BUFSIZ-1] = '\0'; + newline = NULL; + /* nuke the newline if it exists */ + if ((newline = strchr(linebuf, '\n'))) + *newline = '\0'; + if (!strcmp(linebuf, princname)) { + isok = TRUE; + continue; + } + /* clean up the rest of the line if necessary */ + if (!newline) + while (((gobble = getc(fp)) != EOF) && gobble != '\n'); + } + free(princname); + fclose(fp); + return(isok); +} + +static void renewcreds(int cancelled, struct krb5data *data) +{ + int ret; + char ccnambuf[50]; + krb5_ccache tmpcc; + krb5_creds newcreds; + static int ccserial = 0; + + data->renewtimer = NULL; + if(cancelled) + return; + memset(&newcreds, 0, sizeof(newcreds)); + snprintf(ccnambuf, sizeof(ccnambuf), "MEMORY:%i", ccserial++); + if((ret = krb5_cc_resolve(k5context, ccnambuf, &tmpcc)) != 0) + { + flog(LOG_ERR, "could not resolve a temporary ccache `%s': %s", ccnambuf, error_message(ret)); + data->renew = 0; + return; + } + if((ret = krb5_cc_initialize(k5context, tmpcc, data->ticket->enc_part2->client)) != 0) + { + flog(LOG_ERR, "could not initialize temporary ccache: %s", error_message(ret)); + krb5_cc_destroy(k5context, tmpcc); + data->renew = 0; + return; + } + if((ret = krb5_cc_store_cred(k5context, tmpcc, data->creds)) != 0) + { + flog(LOG_ERR, "could not store creds into temporary ccache: %s", error_message(ret)); + krb5_cc_destroy(k5context, tmpcc); + data->renew = 0; + return; + } + if((ret = krb5_get_renewed_creds(k5context, &newcreds, data->ticket->enc_part2->client, tmpcc, NULL)) != 0) + { + flog(LOG_ERR, "could not get renewed tickets for %s: %s", data->username, error_message(ret)); + krb5_cc_destroy(k5context, tmpcc); + data->renew = 0; + return; + } + krb5_free_creds(k5context, data->creds); + data->creds = NULL; + if((ret = krb5_copy_creds(k5context, &newcreds, &data->creds)) != 0) + { + flog(LOG_ERR, "could not copy renewed creds: %s", error_message(ret)); + krb5_cc_destroy(k5context, tmpcc); + data->renew = 0; + return; + } + krb5_free_cred_contents(k5context, &newcreds); + krb5_cc_destroy(k5context, tmpcc); + flog(LOG_ERR, "successfully renewed krb5 creds for %s", data->username); + setrenew(data); +} + +static void setrenew(struct krb5data *data) +{ + krb5_ticket_times times; + time_t now, good; + + times = data->creds->times; + if(!times.starttime) + times.starttime = times.authtime; + now = time(NULL); + if(times.endtime < now) + { + flog(LOG_DEBUG, "tickets already expired, cannot renew"); + data->renew = 0; + return; + } + good = times.starttime + (((times.endtime - times.starttime) * 9) / 10); + data->renewtimer = timercallback(good, (void (*)(int, void *))renewcreds, data); +} + +static int krbauth(struct authhandle *auth, char *passdata) +{ + int ret; + struct krb5data *data; + char *msg; + size_t msglen; + int authorized; + krb5_data k5d; + krb5_flags apopt; + krb5_creds **fwdcreds; + + data = auth->mechdata; + if(passdata == NULL) + { + auth->prompt = AUTH_PR_AUTO; + if(auth->text != NULL) + free(auth->text); + auth->text = swcsdup(L"Send hex-encoded krb5 data"); + data->state = 1; + return(AUTH_PASS); + } else { + if((msg = hexdecode(passdata, &msglen)) == NULL) + { + if(auth->text != NULL) + free(auth->text); + auth->text = swcsdup(L"Invalid hex encoding"); + return(AUTH_DENIED); + } + switch(data->state) + { + case 1: + k5d.length = msglen; + k5d.data = msg; + if((ret = krb5_rd_req(k5context, &data->context, &k5d, myprinc, keytab, &apopt, &data->ticket)) != 0) + { + flog(LOG_INFO, "kerberos authentication failed for %s: %s", data->username, error_message(ret)); + if(auth->text != NULL) + free(auth->text); + auth->text = icmbstowcs((char *)error_message(ret), NULL); + return(AUTH_DENIED); + } + free(msg); + if(apopt & AP_OPTS_MUTUAL_REQUIRED) + { + if((ret = krb5_mk_rep(k5context, data->context, &k5d)) != 0) + { + flog(LOG_WARNING, "krb5_mk_rep returned an error: %s", error_message(ret)); + return(AUTH_ERR); + } + msg = hexencode(k5d.data, k5d.length); + if(auth->text != NULL) + free(auth->text); + auth->text = icmbstowcs(msg, "us-ascii"); + free(msg); + free(k5d.data); + } else { + if(auth->text != NULL) + free(auth->text); + auth->text = swcsdup(L""); + } + data->state = 2; + return(AUTH_PASS); + case 2: + ret = atoi(msg); + free(msg); + if(ret == 1) + { + /* That is, the client has accepted us as a valid + * server. Now check if the client is authorized. */ + if((ret = krb5_unparse_name(k5context, data->ticket->enc_part2->client, &data->cname)) != 0) + { + flog(LOG_ERR, "krb_unparse_name returned an error: %s", error_message(ret)); + return(AUTH_ERR); + } + authorized = 0; + if(!authorized && my_krb5_kuserok(k5context, data->ticket->enc_part2->client, data->username, "/.k5login", 1)) + authorized = 1; + /* Allow a seperate ACL for DC principals */ + if(!authorized && my_krb5_kuserok(k5context, data->ticket->enc_part2->client, data->username, "/.dc-k5login", 0)) + authorized = 1; + if(authorized) + { + flog(LOG_INFO, "krb5 principal %s successfully authorized as %s", data->cname, data->username); + return(AUTH_SUCCESS); + } else { + flog(LOG_INFO, "krb5 principal %s not authorized as %s", data->cname, data->username); + } + } + if(ret == 2) + { + if(auth->text != NULL) + free(auth->text); + auth->text = swcsdup(L""); + data->state = 3; + return(AUTH_PASS); + } + return(AUTH_DENIED); + case 3: + k5d.length = msglen; + k5d.data = msg; + if((ret = krb5_rd_cred(k5context, data->context, &k5d, &fwdcreds, NULL)) != 0) + { + flog(LOG_ERR, "krb5_rd_cred returned an error: %s", error_message(ret)); + return(AUTH_ERR); + } + if(*fwdcreds == NULL) + { + flog(LOG_ERR, "forwarded credentials array was empty (from %s)", data->username); + krb5_free_tgt_creds(k5context, fwdcreds); + return(AUTH_ERR); + } + flog(LOG_INFO, "received forwarded credentials for %s", data->username); + /* Copy only the first credential. (Change this if it becomes a problem) */ + ret = krb5_copy_creds(k5context, *fwdcreds, &data->creds); + krb5_free_tgt_creds(k5context, fwdcreds); + if(ret != 0) + { + flog(LOG_ERR, "could not copy forwarded credentials: %s", error_message(ret)); + return(AUTH_ERR); + } + if(confgetint("auth-krb5", "renewcreds")) + { + data->renew = 1; + setrenew(data); + } + if(auth->text != NULL) + free(auth->text); + auth->text = swcsdup(L""); + data->state = 2; + return(AUTH_PASS); + default: + free(msg); + flog(LOG_ERR, "BUG? Invalid state encountered in krbauth: %i", data->state); + return(AUTH_ERR); + } + } +} + +static int opensess(struct authhandle *auth) +{ + int ret; + struct krb5data *data; + char *buf, *buf2; + int fd; + struct passwd *pwent; + + data = auth->mechdata; + if(data->creds != NULL) + { + if((pwent = getpwnam(data->username)) == NULL) + { + flog(LOG_ERR, "could not get passwd entry for forwarded tickets (user %s): %s", data->username, strerror(errno)); + return(AUTH_ERR); + } + buf = sprintf2("/tmp/krb5cc_dc_%i_XXXXXX", pwent->pw_uid); + if((fd = mkstemp(buf)) < 0) + { + free(buf); + flog(LOG_ERR, "could not create temporary file for ccache: %s", strerror(errno)); + return(AUTH_ERR); + } + close(fd); + buf2 = sprintf2("FILE:%s", buf); + if((ret = krb5_cc_resolve(k5context, buf2, &data->ccache)) != 0) + { + free(buf); + free(buf2); + flog(LOG_ERR, "could not resolve ccache name \"%s\": %s", buf2, error_message(ret)); + return(AUTH_ERR); + } + setenv("KRB5CCNAME", buf2, 1); + free(buf2); + if((ret = krb5_cc_initialize(k5context, data->ccache, data->ticket->enc_part2->client)) != 0) + { + free(buf); + flog(LOG_ERR, "could not initialize ccache: %s", error_message(ret)); + return(AUTH_ERR); + } + if((ret = krb5_cc_store_cred(k5context, data->ccache, data->creds)) != 0) + { + free(buf); + flog(LOG_ERR, "could not store forwarded TGT into ccache: %s", error_message(ret)); + return(AUTH_ERR); + } + if(chown(buf, pwent->pw_uid, pwent->pw_gid)) + { + free(buf); + flog(LOG_ERR, "could not chown new ccache to %i:%i: %s", pwent->pw_uid, pwent->pw_gid, strerror(errno)); + return(AUTH_ERR); + } + free(buf); + } + return(AUTH_SUCCESS); +} + +static int closesess(struct authhandle *auth) +{ + struct krb5data *data; + + data = auth->mechdata; + if(data->ccache != NULL) + { + krb5_cc_destroy(k5context, data->ccache); + data->ccache = NULL; + } + return(AUTH_SUCCESS); +} + +struct authmech authmech_krb5 = +{ + .inithandle = inithandle, + .release = release, + .authenticate = krbauth, + .opensess = opensess, + .closesess = closesess, + .name = L"krb5", + .enabled = 1 +}; + +static int init(int hup) +{ + int ret; + char *buf; + krb5_principal newprinc; + + if(!hup) + { + regmech(&authmech_krb5); + if((ret = krb5_init_context(&k5context))) + { + flog(LOG_CRIT, "could not initialize Kerberos context: %s", error_message(ret)); + return(1); + } + if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL) + { + flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s", confgetstr("auth-krb5", "service"), strerror(errno)); + return(1); + } else { + if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &myprinc)) != 0) + { + flog(LOG_CRIT, "could not get principal for service %s: %s", buf, error_message(ret)); + free(buf); + return(1); + } + free(buf); + } + if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL) + { + flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno)); + keytab = NULL; + } else { + if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0) + { + flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret)); + keytab = NULL; + } + free(buf); + } + } + if(hup) + { + if((buf = icwcstombs(confgetstr("auth-krb5", "service"), NULL)) == NULL) + { + flog(LOG_CRIT, "could not convert service name (%ls) into local charset: %s, not updating principal", confgetstr("auth-krb5", "service"), strerror(errno)); + } else { + if((ret = krb5_sname_to_principal(k5context, NULL, buf, KRB5_NT_SRV_HST, &newprinc)) != 0) + { + flog(LOG_CRIT, "could not get principal for service %s: %s, not updating principal", buf, error_message(ret)); + } else { + krb5_free_principal(k5context, myprinc); + myprinc = newprinc; + } + free(buf); + } + if(keytab != NULL) + krb5_kt_close(k5context, keytab); + if((buf = icwcstombs(confgetstr("auth-krb5", "keytab"), NULL)) == NULL) + { + flog(LOG_ERR, "could not convert keytab name (%ls) into local charset: %s, using default keytab instead", confgetstr("auth-krb5", "keytab"), strerror(errno)); + keytab = NULL; + } else { + if((ret = krb5_kt_resolve(k5context, buf, &keytab)) != 0) + { + flog(LOG_ERR, "could not open keytab %s: %s, using default keytab instead", buf, error_message(ret)); + keytab = NULL; + } + free(buf); + } + } + return(0); +} + +static void terminate(void) +{ + if(keytab != NULL) + krb5_kt_close(k5context, keytab); + krb5_free_principal(k5context, myprinc); + krb5_free_context(k5context); +} + +static struct configvar myvars[] = +{ + {CONF_VAR_STRING, "service", {.str = L"doldacond"}}, + {CONF_VAR_STRING, "keytab", {.str = L""}}, + {CONF_VAR_BOOL, "renewcreds", {.num = 1}}, + {CONF_VAR_END} +}; + +static struct module me = +{ + .conf = + { + .vars = myvars + }, + .init = init, + .terminate = terminate, + .name = "auth-krb5" +}; + +MODULE(me); + +#endif /* HAVE_KRB5 */ diff --git a/daemon/auth-pam.c b/daemon/auth-pam.c new file mode 100644 index 0000000..48dbcbd --- /dev/null +++ b/daemon/auth-pam.c @@ -0,0 +1,332 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +/* + * I have decided that I don't like PAM. Maybe I'm inexperienced, so + * please correct me if I'm wrong, but is it not so that + * pam_authenticate blocks until the user has fully authenticated + * herself? That isn't very good in a program that wants to do other + * things at the same time. In my mind, pam_authenticate should return + * with a conversation struct every time it wants data. + * + * My solution here, for now, is to use the ucontext context switching + * functions to get back and forth from the conversation + * function. Ugly? Yes indeed, it most certainly is, but what am I to + * do, then? If there actually is a good way to do this that is built + * into PAM, _please_, do mail me about it. + */ + +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "auth.h" +#include "utils.h" +#include "conf.h" +#include "log.h" + +struct pamdata +{ + pam_handle_t *pamh; + volatile int pamret; + ucontext_t mainctxt, pamctxt; + void *pamstack; + volatile int validctxt; + volatile int convdone, converr; + volatile char *passdata; +}; + +static int pamconv(int nmsg, const struct pam_message **msg, struct pam_response **resp, struct authhandle *auth) +{ + int i; + struct pamdata *data; + + data = auth->mechdata; + *resp = smalloc(sizeof(**resp) * nmsg); + for(i = 0; i < nmsg; i++) + { + switch(msg[i]->msg_style) + { + case PAM_PROMPT_ECHO_OFF: + auth->prompt = AUTH_PR_NOECHO; + break; + case PAM_PROMPT_ECHO_ON: + auth->prompt = AUTH_PR_ECHO; + break; + case PAM_ERROR_MSG: + auth->prompt = AUTH_PR_ERROR; + break; + case PAM_TEXT_INFO: + auth->prompt = AUTH_PR_INFO; + break; + } + if(auth->text != NULL) + free(auth->text); + if((auth->text = icmbstowcs((char *)msg[i]->msg, NULL)) == NULL) + { + flog(LOG_ERR, "could not convert PAM error %s into wcs: %s", msg[i]->msg, strerror(errno)); + free(*resp); + *resp = NULL; + return(PAM_CONV_ERR); + } + if(swapcontext(&data->pamctxt, &data->mainctxt)) + { + flog(LOG_CRIT, "could not swap context in PAM conversation: %s", strerror(errno)); + free(*resp); + *resp = NULL; + return(PAM_CONV_ERR); + } + if(data->converr) + { + for(; i < nmsg; i++) + { + (*resp)[i].resp = sstrdup(""); + (*resp)[i].resp_retcode = PAM_SUCCESS; + } + return(PAM_CONV_ERR); + } + switch(msg[i]->msg_style) + { + case PAM_PROMPT_ECHO_OFF: + case PAM_PROMPT_ECHO_ON: + (*resp)[i].resp = sstrdup((char *)data->passdata); + memset((void *)data->passdata, 0, strlen((char *)data->passdata)); + (*resp)[i].resp_retcode = PAM_SUCCESS; + break; + } + } + return(PAM_SUCCESS); +} + +static void releasepam(struct pamdata *data) +{ + if(data->pamh != NULL) + { + if(data->validctxt) + { + data->converr = 1; + if(swapcontext(&data->mainctxt, &data->pamctxt)) + { + flog(LOG_CRIT, "could not switch back to PAM context while releasing: %s", strerror(errno)); + return; + } + } + pam_end(data->pamh, data->pamret); + } + if(data->pamstack != NULL) + free(data->pamstack); + free(data); +} + +static void release(struct authhandle *auth) +{ + releasepam((struct pamdata *)auth->mechdata); +} + +static struct pamdata *newpamdata(void) +{ + struct pamdata *new; + + new = smalloc(sizeof(*new)); + new->pamh = NULL; + new->pamret = PAM_SUCCESS; + new->pamstack = NULL; + new->validctxt = 0; + new->converr = 0; + return(new); +} + +static int inithandle(struct authhandle *auth, char *username) +{ + char *buf; + struct pamdata *data; + struct pam_conv conv; + + data = newpamdata(); + conv.conv = (int (*)(int, const struct pam_message **, struct pam_response **, void *))pamconv; + conv.appdata_ptr = auth; + if((buf = icwcstombs(confgetstr("auth", "pamserv"), NULL)) == NULL) + { + flog(LOG_ERR, "could not initialize pam since auth.pamserv cannot be translated into the current locale: %s", strerror(errno)); + releasepam(data); + return(1); + } + if((data->pamret = pam_start(buf, username, &conv, &data->pamh)) != PAM_SUCCESS) + { + flog(LOG_CRIT, "could not pam_start: %s", pam_strerror(NULL, data->pamret)); + releasepam(data); + free(buf); + errno = ENOTSUP; /* XXX */ + return(1); + } + free(buf); + auth->mechdata = data; + return(0); +} + +static void pamauththread(struct authhandle *auth) +{ + struct pamdata *data; + + data = (struct pamdata *)auth->mechdata; + data->validctxt = 1; + data->pamret = pam_authenticate(data->pamh, 0); + data->validctxt = 0; +} + +static int pamauth(struct authhandle *auth, char *passdata) +{ + struct pamdata *data; + + data = auth->mechdata; + if(!data->validctxt) + { + if(getcontext(&data->pamctxt)) + { + flog(LOG_CRIT, "could not get context: %s", strerror(errno)); + return(AUTH_ERR); + } + data->pamctxt.uc_link = &data->mainctxt; + if(data->pamstack == NULL) + data->pamstack = smalloc(65536); + data->pamctxt.uc_stack.ss_sp = data->pamstack; + data->pamctxt.uc_stack.ss_size = 65536; + makecontext(&data->pamctxt, (void (*)(void))pamauththread, 1, auth); + if(swapcontext(&data->mainctxt, &data->pamctxt)) + { + flog(LOG_CRIT, "Could not switch to PAM context: %s", strerror(errno)); + return(AUTH_ERR); + } + if(!data->validctxt) + { + if(data->pamret == PAM_AUTHINFO_UNAVAIL) + return(AUTH_ERR); + else if(data->pamret == PAM_SUCCESS) + return(AUTH_SUCCESS); + else + return(AUTH_DENIED); + } + return(AUTH_PASS); + } else { + data->passdata = passdata; + if(swapcontext(&data->mainctxt, &data->pamctxt)) + { + flog(LOG_CRIT, "could not switch back to PAM context: %s", strerror(errno)); + return(AUTH_ERR); + } + if(!data->validctxt) + { + if(data->pamret == PAM_AUTHINFO_UNAVAIL) + return(AUTH_ERR); + else if(data->pamret == PAM_SUCCESS) + return(AUTH_SUCCESS); + else + return(AUTH_DENIED); + } + return(AUTH_PASS); + } +} + +static int renewcred(struct authhandle *auth) +{ + struct pamdata *data; + + data = auth->mechdata; + if(data->pamh == NULL) + return(AUTH_SUCCESS); + data->pamret = pam_setcred(data->pamh, PAM_REFRESH_CRED); + if(data->pamret != PAM_SUCCESS) + { + flog(LOG_INFO, "could not refresh credentials: %s", pam_strerror(data->pamh, data->pamret)); + return(AUTH_ERR); + } + return(AUTH_SUCCESS); +} + +static int opensess(struct authhandle *auth) +{ + struct pamdata *data; + char **envp; + + data = auth->mechdata; + if(data->pamh == NULL) + { + flog(LOG_ERR, "bug: in auth-pam.c:opensess: called with NULL pamh"); + return(AUTH_ERR); + } + data->pamret = pam_setcred(data->pamh, PAM_ESTABLISH_CRED); + if(data->pamret != PAM_SUCCESS) + { + flog(LOG_INFO, "could not establish credentials: %s", pam_strerror(data->pamh, data->pamret)); + return(AUTH_ERR); + } + data->pamret = pam_open_session(data->pamh, 0); + if(data->pamret != PAM_SUCCESS) + { + flog(LOG_INFO, "could not open session: %s", pam_strerror(data->pamh, data->pamret)); + return(AUTH_ERR); + } + for(envp = pam_getenvlist(data->pamh); *envp; envp++) + putenv(*envp); + return(AUTH_SUCCESS); +} + +static int closesess(struct authhandle *auth) +{ + int rc; + struct pamdata *data; + + data = auth->mechdata; + if(data->pamh == NULL) + { + flog(LOG_ERR, "bug: in auth-pam.c:closesess: called with NULL pamh"); + return(AUTH_ERR); + } + rc = AUTH_SUCCESS; + data->pamret = pam_close_session(data->pamh, 0); + if(data->pamret != PAM_SUCCESS) + { + flog(LOG_INFO, "could not open session: %s", pam_strerror(data->pamh, data->pamret)); + rc = AUTH_ERR; + } + data->pamret = pam_setcred(data->pamh, PAM_DELETE_CRED); + if(data->pamret != PAM_SUCCESS) + { + flog(LOG_INFO, "could not establish credentials: %s", pam_strerror(data->pamh, data->pamret)); + rc = AUTH_ERR; + } + return(rc); +} + +struct authmech authmech_pam = +{ + .inithandle = inithandle, + .release = release, + .authenticate = pamauth, + .renewcred = renewcred, + .opensess = opensess, + .closesess = closesess, + .name = L"pam", + .enabled = 1 +}; diff --git a/daemon/auth.c b/daemon/auth.c new file mode 100644 index 0000000..0d34fbb --- /dev/null +++ b/daemon/auth.c @@ -0,0 +1,183 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "auth.h" +#include "utils.h" +#include "module.h" +#include "conf.h" + +struct authmech *mechs = NULL; + +static int authless_inithandle(struct authhandle *auth, char *username) +{ + return(0); +} + +static void authless_release(struct authhandle *auth) +{ +} + +static int authless_authenticate(struct authhandle *auth, char *data) +{ + return(AUTH_SUCCESS); +} + +static int authless_succeed_1param(struct authhandle *auth) +{ + return(AUTH_SUCCESS); +} + +static struct authmech authless = +{ + .name = L"authless", + .inithandle = authless_inithandle, + .release = authless_release, + .authenticate = authless_authenticate, + .renewcred = authless_succeed_1param, + .opensess = authless_succeed_1param, + .closesess = authless_succeed_1param +}; + +static struct authhandle *newhandle(void) +{ + struct authhandle *auth; + + auth = smalloc(sizeof(*auth)); + auth->refcount = 1; + auth->mech = NULL; + auth->text = NULL; + auth->mechdata = NULL; + return(auth); +} + +void authgethandle(struct authhandle *auth) +{ + auth->refcount++; +} + +void authputhandle(struct authhandle *auth) +{ + if(--auth->refcount) + return; + if(auth->text != NULL) + free(auth->text); + if(auth->mechdata != NULL) + auth->mech->release(auth); + free(auth); +} + +struct authhandle *initauth(wchar_t *mechname, char *username) +{ + struct authmech *mech; + struct authhandle *auth; + + for(mech = mechs; mech != NULL; mech = mech->next) + { + if(mech->enabled && !wcscmp(mechname, mech->name)) + break; + } + if(mech == NULL) + { + errno = ENOENT; + return(NULL); + } + auth = newhandle(); + auth->mech = mech; + if(mech->inithandle(auth, username)) + { + authputhandle(auth); + return(NULL); + } + return(auth); +} + +int authenticate(struct authhandle *handle, char *data) +{ + if(handle->mech == NULL) + return(AUTH_ERR); + return(handle->mech->authenticate(handle, data)); +} + +int authrenewcred(struct authhandle *handle) +{ + if((handle->mech == NULL) || (handle->mech->renewcred == NULL)) + return(AUTH_SUCCESS); + return(handle->mech->renewcred(handle)); +} + +int authopensess(struct authhandle *handle) +{ + if((handle->mech == NULL) || (handle->mech->opensess == NULL)) + return(AUTH_SUCCESS); + return(handle->mech->opensess(handle)); +} + +int authclosesess(struct authhandle *handle) +{ + if((handle->mech == NULL) || (handle->mech->closesess == NULL)) + return(AUTH_SUCCESS); + return(handle->mech->closesess(handle)); +} + +void regmech(struct authmech *mech) +{ + mech->next = mechs; + mechs = mech; +} + +static void preinit(int hup) +{ + extern struct authmech authmech_pam; + + if(hup) + return; + regmech(&authless); + regmech(&authmech_pam); +} + +static int init(int hup) +{ + authless.enabled = confgetint("auth", "authless"); + return(0); +} + +static struct configvar myvars[] = +{ + {CONF_VAR_STRING, "pamserv", {.str = L"doldacond"}}, + {CONF_VAR_BOOL, "authless", {.num = 1}}, + {CONF_VAR_END} +}; + +static struct module me = +{ + .name = "auth", + .conf = + { + .vars = myvars + }, + .preinit = preinit, + .init = init +}; + +MODULE(me) diff --git a/daemon/auth.h b/daemon/auth.h new file mode 100644 index 0000000..0763841 --- /dev/null +++ b/daemon/auth.h @@ -0,0 +1,70 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _AUTH_H +#define _AUTH_H + +#include + +#define AUTH_SUCCESS 0 /* Authentication successful and done */ +#define AUTH_DENIED 1 /* Ultimately failed - reason in handle->text */ +#define AUTH_PASS 2 /* Pass data - look in handle->prompt */ +#define AUTH_ERR 3 /* An error occurred that */ + +#define AUTH_PR_AUTO 0 +#define AUTH_PR_NOECHO 1 +#define AUTH_PR_ECHO 2 +#define AUTH_PR_INFO 3 +#define AUTH_PR_ERROR 4 + +struct authhandle; + +struct authmech +{ + struct authmech *next; + int enabled; + wchar_t *name; + int (*inithandle)(struct authhandle *handle, char *username); + void (*release)(struct authhandle *handle); + int (*authenticate)(struct authhandle *handle, char *data); + int (*renewcred)(struct authhandle *handle); + int (*opensess)(struct authhandle *handle); + int (*closesess)(struct authhandle *handle); +}; + +struct authhandle +{ + int refcount; + struct authmech *mech; + int prompt; + wchar_t *text; + void *mechdata; +}; + +int authenticate(struct authhandle *handle, char *data); +struct authhandle *initauth(wchar_t *mechname, char *username); +void authgethandle(struct authhandle *auth); +void authputhandle(struct authhandle *auth); +int authrenewcred(struct authhandle *handle); +int authopensess(struct authhandle *handle); +int authclosesess(struct authhandle *handle); +void regmech(struct authmech *mech); + +extern struct authmech *mechs; + +#endif diff --git a/daemon/client.c b/daemon/client.c new file mode 100644 index 0000000..e7fa0e7 --- /dev/null +++ b/daemon/client.c @@ -0,0 +1,998 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "client.h" +#include "conf.h" +#include "log.h" +#include "utils.h" +#include "module.h" +#include "tiger.h" +#include "net.h" +#include "sysevents.h" + +struct scanstate +{ + struct scanstate *next; + struct sharecache *node; + DIR *dd; +}; + +struct scanqueue +{ + struct scanqueue *next; + struct scanstate *state; +}; + +static int conf_share(int argc, wchar_t **argv); +static void freecache(struct sharecache *node); +static void checkhashes(void); + +static struct configvar myvars[] = +{ + {CONF_VAR_STRING, "defnick", {.str = L"DoldaConnect user"}}, + {CONF_VAR_INT, "scanfilemask", {.num = 0004}}, + {CONF_VAR_INT, "scandirmask", {.num = 0005}}, + {CONF_VAR_STRING, "hashcache", {.str = L"dc-hashcache"}}, + {CONF_VAR_END} +}; + +static struct configcmd mycmds[] = +{ + {"share", conf_share}, + {NULL} +}; + +static struct scanstate *scanjob = NULL; +static struct scanqueue *scanqueue = NULL; +static struct sharepoint *shares = NULL; +static struct hashcache *hashcache = NULL; +static pid_t hashjob = 0; +struct sharecache *shareroot = NULL; +unsigned long long sharesize = 0; +GCBCHAIN(sharechangecb, unsigned long long); + +static int conf_share(int argc, wchar_t **argv) +{ + struct sharepoint *share; + char *b; + + if(argc < 3) + { + flog(LOG_WARNING, "not enough arguments given for share command"); + return(1); + } + if((b = icwcstombs(argv[2], NULL)) == NULL) + { + flog(LOG_WARNING, "could not convert wcs path (%ls) to current locale's charset: %s", argv[2], strerror(errno)); + return(1); + } + for(share = shares; share != NULL; share = share->next) + { + if(!strcmp(share->path, b) && !wcscmp(share->name, argv[1])) + { + share->delete = 0; + free(b); + return(0); + } + } + share = smalloc(sizeof(*share)); + share->path = b; + share->delete = 0; + share->name = swcsdup(argv[1]); + share->next = shares; + share->prev = NULL; + if(shares != NULL) + shares->prev = share; + shares = share; + return(0); +} + +static void dumpsharecache(struct sharecache *node, int l) +{ + int i; + + for(; node != NULL; node = node->next) + { + for(i = 0; i < l; i++) + putc('\t', stdout); + printf("%ls\n", node->name); + if(node->f.b.type == FILE_DIR) + dumpsharecache(node->child, l + 1); + } +} + +static struct hashcache *newhashcache(void) +{ + struct hashcache *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + new->next = hashcache; + new->prev = NULL; + if(hashcache != NULL) + hashcache->prev = new; + hashcache = new; + return(new); +} + +static void freehashcache(struct hashcache *hc) +{ + if(hc->next != NULL) + hc->next->prev = hc->prev; + if(hc->prev != NULL) + hc->prev->next = hc->next; + if(hc == hashcache) + hashcache = hc->next; + free(hc); +} + +static char *findhashcachefile(int filldef) +{ + static char ret[128]; + char *hcname; + + if(getenv("HOME") != NULL) + { + snprintf(ret, sizeof(ret), "%s/.dc-hashcache", getenv("HOME")); + if(!access(ret, R_OK)) + return(ret); + } + if((hcname = icswcstombs(confgetstr("cli", "hashcache"), NULL, NULL)) == NULL) + { + flog(LOG_WARNING, "could not convert hash cache name into local charset: %s", strerror(errno)); + return(NULL); + } + if(strchr(hcname, '/') != NULL) + { + if(!access(hcname, R_OK)) + { + strcpy(ret, hcname); + return(ret); + } + } else { + snprintf(ret, sizeof(ret), "/etc/%s", hcname); + if(!access(ret, R_OK)) + return(ret); + snprintf(ret, sizeof(ret), "/usr/etc/%s", hcname); + if(!access(ret, R_OK)) + return(ret); + snprintf(ret, sizeof(ret), "/usr/local/etc/%s", hcname); + if(!access(ret, R_OK)) + return(ret); + } + if(filldef) + { + if(getenv("HOME") != NULL) + snprintf(ret, sizeof(ret), "%s/.dc-hashcache", getenv("HOME")); + else + snprintf(ret, sizeof(ret), "/etc/%s", hcname); + return(ret); + } else { + return(NULL); + } +} + +static struct hashcache *findhashcache(dev_t dev, ino_t inode) +{ + struct hashcache *hc; + + for(hc = hashcache; hc != NULL; hc = hc->next) + { + if((hc->dev == dev) && (hc->inode == inode)) + return(hc); + } + return(NULL); +} + +static void readhashcache(void) +{ + int i, wc, line; + char *hcname; + FILE *stream; + char linebuf[256]; + char *p, *p2, *wv[32], *hash; + struct hashcache *hc; + size_t len; + + if((hcname = findhashcachefile(0)) == NULL) + return; + if((stream = fopen(hcname, "r")) == NULL) + { + flog(LOG_WARNING, "could not open hash cache %s: %s", hcname, strerror(errno)); + return; + } + while(hashcache != NULL) + freehashcache(hashcache); + line = 0; + while(!feof(stream)) + { + fgets(linebuf, sizeof(linebuf), stream); + line++; + for(p = linebuf; *p; p++) + { + if(*p == '\n') + *p = ' '; + } + if(linebuf[0] == '#') + continue; + for(wc = 0, p = linebuf; (wc < 32) && ((p2 = strchr(p, ' ')) != NULL); p = p2 + 1) + { + if(p2 == p) + continue; + *p2 = 0; + wv[wc++] = p; + } + if(wc < 3) + continue; + hc = newhashcache(); + hc->dev = strtoll(wv[0], NULL, 10); + hc->inode = strtoll(wv[1], NULL, 10); + hc->mtime = strtoll(wv[2], NULL, 10); + for(i = 3; i < wc; i++) + { + if(!strcmp(wv[i], "tth")) + { + if(++i >= wc) + continue; + hash = base64decode(wv[i], &len); + if(len != 24) + { + free(hash); + continue; + } + memcpy(hc->tth, hash, 24); + free(hash); + } + } + } + fclose(stream); +} + +static void writehashcache(void) +{ + char *buf; + char *hcname; + FILE *stream; + struct hashcache *hc; + + hcname = findhashcachefile(1); + if((stream = fopen(hcname, "w")) == NULL) + { + flog(LOG_WARNING, "could not write hash cache %s: %s", hcname, strerror(errno)); + return; + } + fprintf(stream, "# Dolda Connect hash cache file\n"); + fprintf(stream, "# Generated automatically, do not edit\n"); + fprintf(stream, "# Format: DEVICE INODE MTIME [HASH...]\n"); + fprintf(stream, "# HASH := HASHTYPE HASHVAL\n"); + fprintf(stream, "# HASHTYPE can currently only be `tth'\n"); + for(hc = hashcache; hc != NULL; hc = hc->next) + { + buf = base64encode(hc->tth, 24); + fprintf(stream, "%lli %lli %li tth %s\n", hc->dev, (long long)hc->inode, hc->mtime, buf); + free(buf); + } + fclose(stream); +} + +static void hashread(struct socket *sk, void *uudata) +{ + static char *hashbuf; + static size_t hashbufsize = 0, hashbufdata = 0; + char *buf, *p, *p2, *lp; + size_t bufsize; + char *wv[32]; + int wc; + dev_t dev; + ino_t inode; + time_t mtime; + struct hashcache *hc; + + if((buf = sockgetinbuf(sk, &bufsize)) == NULL) + return; + bufcat(hashbuf, buf, bufsize); + free(buf); + while((lp = memchr(hashbuf, '\n', hashbufdata)) != NULL) + { + *(lp++) = 0; + wc = 0; + p = hashbuf; + while(1) + { + while((p2 = strchr(p, ' ')) == p) + p++; + wv[wc++] = p; + if(p2 == NULL) + { + break; + } else { + *p2 = 0; + p = p2 + 1; + } + } + if(wc != 4) + { + flog(LOG_ERR, "BUG: unexpected number of words (%i) arrived from hashing process", wc); + } else { + dev = strtoll(wv[0], NULL, 10); + inode = strtoll(wv[1], NULL, 10); + mtime = strtol(wv[2], NULL, 10); + if((hc = findhashcache(dev, inode)) == NULL) + { + hc = newhashcache(); + hc->dev = dev; + hc->inode = inode; + } + hc->mtime = mtime; + buf = base64decode(wv[3], NULL); + memcpy(hc->tth, buf, 24); + free(buf); + writehashcache(); + } + memmove(hashbuf, lp, hashbufdata -= (lp - hashbuf)); + } +} + +static void hashexit(pid_t pid, int status, void *uudata) +{ + if(pid != hashjob) + flog(LOG_ERR, "BUG: hashing process changed PID?! old: %i new %i", hashjob, pid); + if(status) + flog(LOG_WARNING, "hashing process exited with non-zero status: %i", status); + hashjob = 0; + checkhashes(); +} + +static int hashfile(char *path) +{ + int i, ret; + int fd; + int pfd[2]; + char buf[4096]; + struct stat sb; + struct tigertreehash tth; + char digest[24]; + struct socket *outsock; + + if((fd = open(path, O_RDONLY)) < 0) + { + flog(LOG_WARNING, "could not open %s for hashing: %s", path, strerror(errno)); + return(1); + } + if(fstat(fd, &sb) < 0) + { + flog(LOG_WARNING, "could not stat %s while hashing: %s", path, strerror(errno)); + close(fd); + return(1); + } + if(pipe(pfd) < 0) + { + flog(LOG_WARNING, "could not create pipe(!): %s", strerror(errno)); + close(fd); + return(1); + } + hashjob = fork(); + if(hashjob < 0) + { + flog(LOG_WARNING, "could not fork(!) hashing process: %s", strerror(errno)); + close(fd); + close(pfd[0]); + close(pfd[1]); + return(1); + } + if(hashjob == 0) + { + nice(10); + signal(SIGHUP, SIG_DFL); + fd = dup2(fd, 4); + pfd[1] = dup2(pfd[1], 3); + dup2(fd, 0); + dup2(pfd[1], 1); + for(i = 3; i < FD_SETSIZE; i++) + close(i); + initlog(); + inittigertree(&tth); + while((ret = read(0, buf, 4096)) > 0) + dotigertree(&tth, buf, ret); + if(ret < 0) + { + flog(LOG_WARNING, "could not read from %s while hashing: %s", path, strerror(errno)); + exit(1); + } + synctigertree(&tth); + restigertree(&tth, digest); + ret = snprintf(buf, sizeof(buf), "%lli %lli %li %s\n", sb.st_dev, (long long)sb.st_ino, sb.st_mtime, base64encode(digest, 24)); + write(1, buf, ret); + exit(0); + } + close(fd); + close(pfd[1]); + outsock = wrapsock(pfd[0]); + outsock->readcb = hashread; + childcallback(hashjob, hashexit, NULL); + return(0); +} + +/* + * Call only when hashjob == 0 + */ +static void checkhashes(void) +{ + struct sharecache *node; + struct hashcache *hc; + char *path; + + node = shareroot->child; + while(1) + { + if(node->child != NULL) + { + node = node->child; + continue; + } + if(!node->f.b.hastth) + { + if((hc = findhashcache(node->dev, node->inode)) != NULL) + { + memcpy(node->hashtth, hc->tth, 24); + node->f.b.hastth = 1; + GCBCHAINDOCB(sharechangecb, sharesize); + } else { + path = getfspath(node); + if(hashfile(path)) + { + flog(LOG_WARNING, "could not hash %s, unsharing it", path); + freecache(node); + } + free(path); + return; + } + } + while(node->next == NULL) + { + if((node = node->parent) == shareroot) + break; + } + if(node == shareroot) + break; + node = node->next; + } +} + +struct sharecache *nextscnode(struct sharecache *node) +{ + if(node->child != NULL) + return(node->child); + while(node->next == NULL) + { + node = node->parent; + if(node == shareroot) + return(NULL); + } + return(node->next); +} + +static void freescan(struct scanstate *job) +{ + if(job->dd != NULL) + closedir(job->dd); + free(job); +} + +/* No need for optimization; lookup isn't really that common */ +struct sharecache *findcache(struct sharecache *parent, wchar_t *name) +{ + struct sharecache *node; + + for(node = parent->child; node != NULL; node = node->next) + { + if(!wcscmp(node->name, name)) + return(node); + } + return(NULL); +} + +static void attachcache(struct sharecache *parent, struct sharecache *node) +{ + node->parent = parent; + node->next = parent->child; + if(parent->child != NULL) + parent->child->prev = node; + parent->child = node; +} + +static void detachcache(struct sharecache *node) +{ + if(node->next != NULL) + node->next->prev = node->prev; + if(node->prev != NULL) + node->prev->next = node->next; + if((node->parent != NULL) && (node->parent->child == node)) + node->parent->child = node->next; + node->parent = NULL; + node->next = NULL; + node->prev = NULL; +} + +static void freecache(struct sharecache *node) +{ + struct sharecache *cur, *next; + struct scanqueue *q, *nq, **fq; + + detachcache(node); + fq = &scanqueue; + for(q = scanqueue; q != NULL; q = nq) + { + nq = q->next; + if(q->state->node == node) + { + flog(LOG_DEBUG, "freed node %ls cancelled queued scan", node->name); + freescan(q->state); + *fq = q->next; + free(q); + continue; + } + fq = &q->next; + } + if(node->child != NULL) + { + for(cur = node->child; cur != NULL; cur = next) + { + next = cur->next; + freecache(cur); + } + } + CBCHAINDOCB(node, share_delete, node); + CBCHAINFREE(node, share_delete); + sharesize -= node->size; + if(node->path != NULL) + free(node->path); + if(node->name != NULL) + free(node->name); + free(node); +} + +static void freesharepoint(struct sharepoint *share) +{ + struct sharecache *node; + + if(share->next != NULL) + share->next->prev = share->prev; + if(share->prev != NULL) + share->prev->next = share->next; + if(share == shares) + shares = share->next; + if((node = findcache(shareroot, share->name)) != NULL) + freecache(node); + free(share->path); + free(share->name); + free(share); +} + +static struct sharecache *newcache(void) +{ + struct sharecache *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + CBCHAININIT(new, share_delete); + return(new); +} + +char *getfspath(struct sharecache *node) +{ + char *buf, *mbsname; + size_t bufsize; + + buf = smalloc(bufsize = 64); + *buf = 0; + while(node != NULL) + { + if(node->path != NULL) + { + if(bufsize < strlen(node->path) + strlen(buf) + 1) + buf = srealloc(buf, strlen(node->path) + strlen(buf) + 1); + memmove(buf + strlen(node->path), buf, strlen(buf) + 1); + memcpy(buf, node->path, strlen(node->path)); + return(buf); + } + if((mbsname = icwcstombs(node->name, NULL)) == NULL) + { + flog(LOG_WARNING, "could not map unicode share name (%ls) into filesystem charset: %s", node->name, strerror(errno)); + free(buf); + return(NULL); + } + while(bufsize < strlen(mbsname) + 1 + strlen(buf) + 1) + buf = srealloc(buf, bufsize *= 2); + memmove(buf + strlen(mbsname) + 1, buf, strlen(buf) + 1); + memcpy(buf + 1, mbsname, strlen(mbsname)); + *buf = '/'; + free(mbsname); + node = node->parent; + } + buf = srealloc(buf, strlen(buf) + 1); + return(buf); +} + +static int checknode(struct sharecache *node) +{ + char *path; + struct stat sb; + + if(node->parent == NULL) + { + return(1); + } else { + if(!checknode(node->parent)) + return(0); + path = getfspath(node); + if(stat(path, &sb) < 0) + { + flog(LOG_INFO, "%s was found to be broken (%s); scheduling rescan of parent", path, strerror(errno)); + queuescan(node->parent); + return(0); + } else { + return(1); + } + } +} + +int opensharecache(struct sharecache *node) +{ + char *path; + int fd, errbak; + + path = getfspath(node); + fd = open(path, O_RDONLY); + errbak = errno; + if(fd < 0) + { + flog(LOG_WARNING, "could not open %s: %s", path, strerror(errbak)); + checknode(node); + } + free(path); + errno = errbak; + return(fd); +} + +static struct scanstate *newscan(struct sharecache *node) +{ + struct scanstate *new; + + new = smalloc(sizeof(*new)); + new->next = NULL; + new->node = node; + new->dd = NULL; + return(new); +} + +void queuescan(struct sharecache *node) +{ + struct scanqueue *new; + + new = smalloc(sizeof(*new)); + new->state = newscan(node); + new->next = scanqueue; + scanqueue = new; +} + +/* For internal use in doscan() */ +static void removestale(struct sharecache *node) +{ + struct sharecache *cur, *next; + + for(cur = node->child; cur != NULL; cur = next) + { + next = cur->next; + if(!cur->f.b.found) + freecache(cur); + } +} + +/* For internal use in doscan() */ +static void jobdone(void) +{ + struct scanstate *jbuf; + + jbuf = scanjob; + scanjob = jbuf->next; + freescan(jbuf); + if(scanjob != NULL) + fchdir(dirfd(scanjob->dd)); +} + +int doscan(int quantum) +{ + char *path; + wchar_t *wcs; + int type; + struct sharecache *n; + struct scanstate *jbuf; + struct scanqueue *qbuf; + struct dirent *de; + struct stat sb; + struct hashcache *hc; + int dmask, fmask; + static int busybefore = 0; + + dmask = confgetint("cli", "scandirmask"); + fmask = confgetint("cli", "scanfilemask"); + if((scanjob != NULL) && (scanjob->dd != NULL)) + { + while(fchdir(dirfd(scanjob->dd)) < 0) + { + flog(LOG_WARNING, "could not fchdir to fd %i: %s", dirfd(scanjob->dd), strerror(errno)); + removestale(scanjob->node); + jobdone(); + } + } + while(quantum-- > 0) + { + if(scanjob != NULL) + { + busybefore = 1; + } else { + while(scanjob == NULL) + { + if(scanqueue == NULL) + { + if(busybefore) + { + flog(LOG_INFO, "sharing %lli bytes", sharesize); + busybefore = 0; + GCBCHAINDOCB(sharechangecb, sharesize); + if(hashjob == 0) + checkhashes(); + } + return(0); + } + busybefore = 1; + scanjob = scanqueue->state; + qbuf = scanqueue; + scanqueue = qbuf->next; + free(qbuf); + for(n = scanjob->node->child; n != NULL; n = n->next) + n->f.b.found = 0; + } + } + if(scanjob->dd == NULL) + { + path = getfspath(scanjob->node); + if((scanjob->dd = opendir(path)) == NULL) + { + flog(LOG_WARNING, "cannot open directory %s for scanning: %s, deleting from share", path, strerror(errno)); + freecache(scanjob->node); + free(path); + jobdone(); + continue; + } + free(path); + if(fchdir(dirfd(scanjob->dd)) < 0) + { + flog(LOG_WARNING, "could not fchdir to fd %i: %s", dirfd(scanjob->dd), strerror(errno)); + jobdone(); + continue; + } + } + if((de = readdir(scanjob->dd)) == NULL) + { + removestale(scanjob->node); + jobdone(); + continue; + } + if(*de->d_name == '.') + continue; + if((wcs = icmbstowcs(de->d_name, NULL)) == NULL) + { + flog(LOG_WARNING, "file name %s has cannot be converted to wchar: %s", de->d_name, strerror(errno)); + continue; + } + n = findcache(scanjob->node, wcs); + if(stat(de->d_name, &sb) < 0) + { + free(wcs); + if(n != NULL) + { + flog(LOG_WARNING, "could not stat %s: %s, deleting from share", de->d_name, strerror(errno)); + freecache(n); + } else { + flog(LOG_WARNING, "could not stat %s: %s", de->d_name, strerror(errno)); + } + continue; + } + if(S_ISDIR(sb.st_mode)) + { + if(~sb.st_mode & dmask) + { + free(wcs); + continue; + } + type = FILE_DIR; + } else if(S_ISREG(sb.st_mode)) { + if(~sb.st_mode & fmask) + { + free(wcs); + continue; + } + type = FILE_REG; + } else { + flog(LOG_WARNING, "unhandled file type: %i", sb.st_mode); + free(wcs); + continue; + } + if(n != NULL) + { + if((n->f.b.type != type) || (n->mtime != sb.st_mtime) || ((type == FILE_REG) && (n->size != sb.st_size))) + { + freecache(n); + n = NULL; + } + } + if(n == NULL) + { + n = newcache(); + n->name = wcs; + if(S_ISREG(sb.st_mode)) + { + sharesize += (n->size = sb.st_size); + } else { + n->size = 0; + } + n->mtime = sb.st_mtime; + n->dev = sb.st_dev; + n->inode = sb.st_ino; + n->f.b.type = type; + attachcache(scanjob->node, n); + } else { + free(wcs); + } + n->f.b.found = 1; + if(n->f.b.type == FILE_DIR) + { + jbuf = newscan(n); + jbuf->next = scanjob; + scanjob = jbuf; + } else if(n->f.b.type == FILE_REG) { + if(n->f.b.hastth && (n->mtime != sb.st_mtime)) + n->f.b.hastth = 0; + if(!n->f.b.hastth) + { + if((hc = findhashcache(sb.st_dev, sb.st_ino)) != NULL) + { + if(hc->mtime == n->mtime) + { + n->f.b.hastth = 1; + memcpy(n->hashtth, hc->tth, 24); + } else { + freehashcache(hc); + } + } + } + } + } + return(1); +} + +void scanshares(void) +{ + struct sharepoint *cur; + struct sharecache *node; + struct stat sb; + + for(cur = shares; cur != NULL; cur = cur->next) + { + if((node = findcache(shareroot, cur->name)) == NULL) + { + if(stat(cur->path, &sb)) + { + flog(LOG_WARNING, "could not stat share \"%ls\": %s", cur->name, strerror(errno)); + continue; + } + if(!S_ISDIR(sb.st_mode)) + { + flog(LOG_WARNING, "%s is not a directory; won't share it", cur->path); + continue; + } + node = newcache(); + node->name = swcsdup(cur->name); + node->path = sstrdup(cur->path); + if(node->path[strlen(node->path) - 1] == '/') + node->path[strlen(node->path) - 1] = 0; + node->f.b.type = FILE_DIR; + attachcache(shareroot, node); + } + queuescan(node); + } +} + +static void preinit(int hup) +{ + struct sharepoint *cur; + + if(hup) + { + for(cur = shares; cur != NULL; cur = cur->next) + cur->delete = 1; + } else { + shareroot = newcache(); + shareroot->name = swcsdup(L""); + shareroot->f.b.type = FILE_DIR; + } +} + +static int init(int hup) +{ + struct sharepoint *cur, *next; + + readhashcache(); + for(cur = shares; cur != NULL; cur = next) + { + next = cur->next; + if(cur->delete) + freesharepoint(cur); + } + scanshares(); + if(!hup) + while(doscan(100)); + return(0); +} + +static int run(void) +{ + return(doscan(10)); +} + +static void terminate(void) +{ + if(hashjob != 0) + kill(hashjob, SIGHUP); + while(shares != NULL) + freesharepoint(shares); + freecache(shareroot); +} + +static struct module me = +{ + .name = "cli", + .conf = + { + .vars = myvars, + .cmds = mycmds + }, + .preinit = preinit, + .init = init, + .run = run, + .terminate = terminate +}; + +MODULE(me) diff --git a/daemon/client.h b/daemon/client.h new file mode 100644 index 0000000..7aee5cf --- /dev/null +++ b/daemon/client.h @@ -0,0 +1,85 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _CLIENT_H +#define _CLIENT_H + +#include + +#include + +#define FILE_REG 0 +#define FILE_DIR 1 +#define FILE_INT 2 + +#define HASHHASHSIZE 12 + +struct sharepoint +{ + struct sharepoint *prev, *next; + char *path; + wchar_t *name; + int delete; +}; + +struct hashcache +{ + struct hashcache *next, *prev; + dev_t dev; + ino_t inode; + time_t mtime; + char tth[24]; +}; + +struct sharecache +{ + struct sharecache *next, *prev, *child, *parent; + char *path; + wchar_t *name; + size_t size; + time_t mtime; + dev_t dev; + ino_t inode; + char hashtth[24]; + union + { + struct + { + int type:3; + int hastth:1; + int found:1; + } b; + int w; + } f; + CBCHAIN(share_delete, struct sharecache *); +}; + +void clientpreinit(void); +int clientinit(void); +int doscan(int quantum); +int opensharecache(struct sharecache *node); +struct sharecache *findcache(struct sharecache *parent, wchar_t *name); +void queuescan(struct sharecache *node); +char *getfspath(struct sharecache *node); +struct sharecache *nextscnode(struct sharecache *node); + +extern struct sharecache *shareroot; +extern unsigned long long sharesize; +EGCBCHAIN(sharechangecb, unsigned long long); + +#endif diff --git a/daemon/conf.c b/daemon/conf.c new file mode 100644 index 0000000..ea3229e --- /dev/null +++ b/daemon/conf.c @@ -0,0 +1,392 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "conf.h" +#include "log.h" +#include "utils.h" + +static struct configmod *modules = NULL; + +#if 0 +static void dumpconfig(void) +{ + struct configmod *mod; + struct configvar *var; + + for(mod = modules; mod != NULL; mod = mod->next) + { + printf("%s:\n", mod->name); + for(var = mod->vars; var->type != CONF_VAR_END; var++) + { + switch(var->type) + { + case CONF_VAR_BOOL: + printf("\t%s: %s\n", var->name, var->val.num?"t":"f"); + break; + case CONF_VAR_INT: + printf("\t%s: %i\n", var->name, var->val.num); + break; + case CONF_VAR_STRING: + printf("\t%s: \"%ls\" (%i)\n", var->name, var->val.str, wcslen(var->val.str)); + break; + case CONF_VAR_IPV4: + printf("\t%s: %s\n", var->name, inet_ntoa(var->val.ipv4)); + break; + } + } + } +} +#endif + +struct configvar *confgetvar(char *modname, char *varname) +{ + struct configmod *m; + struct configvar *v; + + for(m = modules; m != NULL; m = m->next) + { + if(!strcmp(m->name, modname)) + { + for(v = m->vars; v->type != CONF_VAR_END; v++) + { + if(!strcmp(v->name, varname)) + return(v); + } + break; + } + } + return(NULL); +} + +void confregmod(struct configmod *mod) +{ + struct configvar *var; + + for(var = mod->vars; var->type != CONF_VAR_END; var++) + { + switch(var->type) + { + case CONF_VAR_BOOL: + case CONF_VAR_INT: + var->val.num = var->defaults.num; + break; + case CONF_VAR_STRING: + if(var->defaults.str != NULL) + { + var->val.str = swcsdup(var->defaults.str); + } else { + var->val.str = NULL; + } + break; + case CONF_VAR_IPV4: + var->val.ipv4.s_addr = var->defaults.ipv4.s_addr; + break; + } + CBCHAININIT(var, conf_update); + } + mod->next = modules; + modules = mod; +} + +int runconfcmd(int argc, wchar_t **argv) +{ + struct configmod *module; + struct configvar *var; + struct configcmd *cmd; + int ret, handled; + wchar_t *p; + char *cmdn, *buf, *buf2, *valbuf; + long num; + struct in_addr newipv4; + int cb; + + if(argc < 1) + return(0); + if((cmdn = icwcstombs(argv[0], "us-ascii")) == NULL) + { + flog(LOG_WARNING, "could not convert %ls to us-ascii", argv[0]); + return(1); + } + ret = 1; + handled = 0; + if(!strcmp(cmdn, "set")) + { + handled = 1; + ret = 0; + if((p = wcschr(argv[1], L'.')) == NULL) + { + flog(LOG_WARNING, "illegal configuration variable format: %ls", argv[1]); + errno = EINVAL; + free(cmdn); + return(1); + } + *(p++) = L'\0'; + if((buf = icwcstombs(argv[1], "us-ascii")) == NULL) + { + flog(LOG_WARNING, "could not convert %ls to us-ascii", argv[1]); + free(cmdn); + return(1); + } + if((buf2 = icwcstombs(p, "us-ascii")) == NULL) + { + free(buf); + flog(LOG_WARNING, "could not convert %ls to us-ascii", p); + free(cmdn); + return(1); + } + for(module = modules; module != NULL; module = module->next) + { + if(!strcmp(module->name, buf) && (module->vars != NULL)) + { + for(var = module->vars; var->type != CONF_VAR_END; var++) + { + if(!strcmp(var->name, buf2)) + { + cb = 0; + switch(var->type) + { + case CONF_VAR_BOOL: + wcstolower(argv[2]); + if(!wcscmp(argv[2], L"off") || + !wcscmp(argv[2], L"false") || + !wcscmp(argv[2], L"no") || + !wcscmp(argv[2], L"0")) + { + if(var->val.num) + cb = 1; + var->val.num = 0; + } else if(!wcscmp(argv[2], L"on") || + !wcscmp(argv[2], L"true") || + !wcscmp(argv[2], L"yes") || + !wcscmp(argv[2], L"1")) { + if(!var->val.num) + cb = 1; + var->val.num = 1; + } else { + flog(LOG_WARNING, "unrecognized boolean: %ls", argv[2]); + } + break; + case CONF_VAR_INT: + num = wcstol(argv[2], &p, 0); + if(p == argv[2]) + { + flog(LOG_WARNING, "%ls: not a number, ignoring", argv[2]); + ret = 1; + } else { + if(*p != L'\0') + flog(LOG_WARNING, "%ls: could not entirely parse as a number, ignoring trailing garbage", argv[2]); + if(num != var->val.num) + cb = 1; + var->val.num = num; + } + break; + case CONF_VAR_STRING: + if(wcscmp(var->val.str, argv[2])) + cb = 1; + free(var->val.str); + var->val.str = swcsdup(argv[2]); + break; + case CONF_VAR_IPV4: + if((valbuf = icwcstombs(argv[2], "us-ascii")) == NULL) + { + flog(LOG_WARNING, "could not convert IPv4 address to as-ascii in var %s, ignoring", buf2); + } else { + if(!inet_aton(valbuf, &newipv4)) + { + flog(LOG_WARNING, "could not parse IPv4 address (%s), ignoring", valbuf); + memcpy(&var->val.ipv4, &var->defaults.ipv4, sizeof(var->val.ipv4)); + } else { + if(memcmp(&newipv4, &var->val.ipv4, sizeof(newipv4))) + cb = 1; + memcpy(&var->val.ipv4, &newipv4, sizeof(newipv4)); + } + free(valbuf); + } + break; + } + if(cb) + CBCHAINDOCB(var, conf_update, var); + break; + } + } + if(var == NULL) + flog(LOG_WARNING, "variable %s not found, ignoring set command", buf2); + break; + } + } + if(module == NULL) + flog(LOG_WARNING, "module %s not found, ignoring set command", buf); + free(buf2); + free(buf); + } + for(module = modules; !handled && (module != NULL); module = module->next) + { + if(module->cmds != NULL) + { + for(cmd = module->cmds; cmd->name != NULL; cmd++) + { + if(!strcmp(cmd->name, cmdn)) + { + handled = 1; + ret = cmd->handler(argc, argv); + break; + } + } + } + } + if(!handled) + flog(LOG_WARNING, "command not found: %s", cmdn); + free(cmdn); + return(ret); +} + +char *findconfigfile(void) +{ + static char pathbuf[128]; + char *p, *p2; + + if(getenv("HOME") != NULL) + { + snprintf(pathbuf, sizeof(pathbuf), "%s/.doldacond", getenv("HOME")); + if(!access(pathbuf, R_OK)) + return(pathbuf); + } + p = CONFIG_PATH; + do + { + p2 = strchr(p, ':'); + if(p2 != NULL) + { + memcpy(pathbuf, p, p2 - p); + pathbuf[p2 - p] = 0; + if(!access(pathbuf, R_OK)) + return(pathbuf); + } else { + if(!access(p, R_OK)) + return(p); + } + p = p2 + 1; + } while(p2 != NULL); + return(NULL); +} + +void readconfig(FILE *stream) +{ + int state; + wint_t c; + wchar_t *words[16]; + wchar_t *buf, *p, *p2; + int w; + int line; + + buf = smalloc(sizeof(wchar_t) * 1024); + state = 0; + c = getwc(stream); + w = 0; + line = 1; + p = buf; + while(c != WEOF) + { + if(c == '#') + { + do + c = getwc(stream); + while((c != WEOF) && (c != L'\n')); + continue; + } + switch(state) + { + case 0: + if(iswspace(c)) + { + if(c == L'\n') + { + line++; + if(runconfcmd(w, words)) + flog(LOG_WARNING, "ignoring this command on line %i", line); + w = 0; + } + c = getwc(stream); + } else { + state = 1; + p2 = p; + } + break; + case 1: + if(c == L'\"') + { + state = 2; + c = getwc(stream); + } else if(iswspace(c)) { + if(w >= 16) + { + flog(LOG_WARNING, "too many words on config line %i, ignoring rest", line); + } else { + *(p++) = L'\0'; + words[w++] = p2; + } + state = 0; + } else { + if(c == L'\\') + c = getwc(stream); + if(p - buf < 1023) + *(p++) = c; + else + flog(LOG_WARNING, "too many characters on config line %i, ignoring rest", line); + c = getwc(stream); + } + break; + case 2: + if(c == L'\"') + { + c = getwc(stream); + state = 1; + } else { + if(c == L'\\') + c = getwc(stream); + if(p - buf < 1023) + *(p++) = c; + else + flog(LOG_WARNING, "too many characters on config line %i, ignoring rest", line); + c = getwc(stream); + } + break; + } + } + free(buf); + if(ferror(stream)) + flog(LOG_WARNING, "error on configuration stream: %s", strerror(errno)); + if(state != 0) + flog(LOG_WARNING, "unexpected end of file"); +} diff --git a/daemon/conf.h b/daemon/conf.h new file mode 100644 index 0000000..7edc835 --- /dev/null +++ b/daemon/conf.h @@ -0,0 +1,75 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _CONF_H +#define _CONF_H + +#include +#include +#include +#include + +#define CONFIG_PATH "/usr/local/etc/doldacond.conf:/usr/etc/doldacond.conf:/etc/doldacond.conf" + +#define CONF_VAR_END -1 +#define CONF_VAR_BOOL 0 +#define CONF_VAR_INT 1 +#define CONF_VAR_STRING 2 +#define CONF_VAR_IPV4 3 + +struct configvar +{ + int type; + char *name; + union + { + int num; + wchar_t *str; + struct in_addr ipv4; + } defaults; + union + { + int num; + wchar_t *str; + struct in_addr ipv4; + } val; + CBCHAIN(conf_update, struct configvar *); +}; + +struct configcmd +{ + char *name; + int (*handler)(int argc, wchar_t **argv); +}; + +struct configmod +{ + struct configmod *next; + char *name; + struct configvar *vars; + struct configcmd *cmds; +}; + +struct configvar *confgetvar(char *modname, char *varname); +#define confgetint(m, v) (confgetvar((m), (v))->val.num) +#define confgetstr(m, v) (confgetvar((m), (v))->val.str) +void confregmod(struct configmod *mod); +void readconfig(FILE *stream); +char *findconfigfile(void); + +#endif diff --git a/daemon/emacs-local b/daemon/emacs-local new file mode 100644 index 0000000..f7263a1 --- /dev/null +++ b/daemon/emacs-local @@ -0,0 +1,13 @@ +; -*-Lisp-*- + +; Use with: +; (add-hook 'find-file-hooks +; (lambda () +; (load (concat default-directory "emacs-local") t))) + +(if + (string-match "\\.[ch]$" (buffer-file-name (current-buffer))) + (progn + (make-local-variable 'compile-command) + (setq compile-command "make -k 'CFLAGS=-g -Wall'") +)) diff --git a/daemon/filenet.c b/daemon/filenet.c new file mode 100644 index 0000000..81a83c4 --- /dev/null +++ b/daemon/filenet.c @@ -0,0 +1,473 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "filenet.h" +#include "search.h" +#include "module.h" +#include "utils.h" +#include "net.h" + +static struct fnet *networks = NULL; +struct fnetnode *fnetnodes = NULL; +int numfnetnodes = 0; +GCBCHAIN(newfncb, struct fnetnode *); + +static struct fnetnode *newfn(struct fnet *fnet) +{ + static int curid = 0; + struct fnetnode *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + new->fnet = fnet; + new->refcount = 1; + new->id = curid++; + new->mynick = swcsdup(confgetstr("cli", "defnick")); + new->srchwait = confgetint("fnet", "srchwait"); + new->state = FNN_SYN; + CBCHAININIT(new, fnetnode_ac); + CBCHAININIT(new, fnetnode_chat); + CBCHAININIT(new, fnetnode_unlink); + CBCHAININIT(new, fnetnode_destroy); + new->next = NULL; + new->prev = NULL; + numfnetnodes++; + return(new); +} + +void killfnetnode(struct fnetnode *fn) +{ + fnetsetstate(fn, FNN_DEAD); + if(fn->sk != NULL) + { + fn->sk->close = 1; + if(fn->sk->data == fn) + putfnetnode(fn); + putsock(fn->sk); + fn->sk = NULL; + } +} + +void getfnetnode(struct fnetnode *fn) +{ + fn->refcount++; +#ifdef DEBUG + fprintf(stderr, "getfnetnode on id %i at %p, refcount=%i\n", fn->id, fn, fn->refcount); +#endif +} + +void putfnetnode(struct fnetnode *fn) +{ + struct fnetnode *cur; + +#ifdef DEBUG + fprintf(stderr, "putfnetnode on id %i at %p, refcount=%i\n", fn->id, fn, fn->refcount - 1); +#endif + if(--fn->refcount) + return; + for(cur = fnetnodes; cur != NULL; cur = cur->next) + { + if(cur == fn) + flog(LOG_CRIT, "BUG: fnetnode reached refcount 0 while still in list - id %i", fn->id); + } + CBCHAINDOCB(fn, fnetnode_destroy, fn); + CBCHAINFREE(fn, fnetnode_ac); + CBCHAINFREE(fn, fnetnode_chat); + CBCHAINFREE(fn, fnetnode_unlink); + CBCHAINFREE(fn, fnetnode_destroy); + if(fn->fnet->destroy != NULL) + fn->fnet->destroy(fn); + while(fn->peers != NULL) + fnetdelpeer(fn->peers); + if(fn->mynick != NULL) + free(fn->mynick); + if(fn->name != NULL) + free(fn->name); + if(fn->sk != NULL) + putsock(fn->sk); + free(fn); + numfnetnodes--; +} + +struct fnetnode *findfnetnode(int id) +{ + struct fnetnode *fn; + + for(fn = fnetnodes; (fn != NULL) && (fn->id != id); fn = fn->next); + return(fn); +} + +void linkfnetnode(struct fnetnode *fn) +{ + if(fn->linked) + return; + getfnetnode(fn); + fn->next = fnetnodes; + if(fnetnodes != NULL) + fnetnodes->prev = fn; + fnetnodes = fn; + fn->linked = 1; + GCBCHAINDOCB(newfncb, fn); +} + +void unlinkfnetnode(struct fnetnode *fn) +{ + if(!fn->linked) + return; + if(fnetnodes == fn) + fnetnodes = fn->next; + if(fn->next != NULL) + fn->next->prev = fn->prev; + if(fn->prev != NULL) + fn->prev->next = fn->next; + fn->linked = 0; + CBCHAINDOCB(fn, fnetnode_unlink, fn); + putfnetnode(fn); +} + +static void conncb(struct socket *sk, int err, struct fnetnode *data) +{ + if(err != 0) + { + killfnetnode(data); + putfnetnode(data); + return; + } + data->sk = sk; + fnetsetstate(data, FNN_HS); + socksettos(sk, confgetint("fnet", "fntos")); + data->fnet->connect(data); + putfnetnode(data); +} + +static void resolvecb(struct sockaddr *addr, int addrlen, struct fnetnode *data) +{ + if(addr == NULL) + { + killfnetnode(data); + putfnetnode(data); + } else { + netcsconn(addr, addrlen, (void (*)(struct socket *, int, void *))conncb, data); + } +} + +static struct fnetpeerdatum *finddatum(struct fnetnode *fn, wchar_t *id) +{ + struct fnetpeerdatum *datum; + + for(datum = fn->peerdata; datum != NULL; datum = datum->next) + { + if(!wcscmp(datum->id, id)) + break; + } + return(datum); +} + +static struct fnetpeerdatum *adddatum(struct fnetnode *fn, wchar_t *id, int datatype) +{ + struct fnetpeerdatum *new; + + new = smalloc(sizeof(*new)); + new->refcount = 0; + new->id = swcsdup(id); + new->datatype = datatype; + new->prev = NULL; + new->next = fn->peerdata; + if(fn->peerdata != NULL) + fn->peerdata->prev = new; + fn->peerdata = new; + return(new); +} + +static struct fnetpeerdi *difindoradd(struct fnetpeer *peer, struct fnetpeerdatum *datum) +{ + int i; + + for(i = 0; i < peer->dinum; i++) + { + if(peer->peerdi[i].datum == datum) + break; + } + if(i >= peer->dinum) + { + peer->peerdi = srealloc(peer->peerdi, sizeof(struct fnetpeerdi) * (peer->dinum + 1)); + memset(&peer->peerdi[peer->dinum], 0, sizeof(struct fnetpeerdi)); + peer->peerdi[peer->dinum].datum = datum; + datum->refcount++; + return(&peer->peerdi[peer->dinum++]); + } else { + return(&peer->peerdi[i]); + } +} + +void fnetpeersetstr(struct fnetpeer *peer, wchar_t *id, wchar_t *value) +{ + struct fnetpeerdatum *datum; + struct fnetpeerdi *di; + + if((datum = finddatum(peer->fn, id)) == NULL) + datum = adddatum(peer->fn, id, FNPD_STR); + di = difindoradd(peer, datum); + if(di->data.str != NULL) + free(di->data.str); + di->data.str = swcsdup(value); +} + +void fnetpeersetnum(struct fnetpeer *peer, wchar_t *id, int value) +{ + struct fnetpeerdatum *datum; + struct fnetpeerdi *di; + + if((datum = finddatum(peer->fn, id)) == NULL) + datum = adddatum(peer->fn, id, FNPD_INT); + di = difindoradd(peer, datum); + di->data.num = value; +} + +void fnetpeersetlnum(struct fnetpeer *peer, wchar_t *id, long long value) +{ + struct fnetpeerdatum *datum; + struct fnetpeerdi *di; + + if((datum = finddatum(peer->fn, id)) == NULL) + datum = adddatum(peer->fn, id, FNPD_LL); + di = difindoradd(peer, datum); + di->data.lnum = value; +} + +static void putdatum(struct fnetpeer *peer, struct fnetpeerdatum *datum) +{ + if(--datum->refcount > 0) + return; + if(datum->next != NULL) + datum->next->prev = datum->prev; + if(datum->prev != NULL) + datum->prev->next = datum->next; + if(datum == peer->fn->peerdata) + peer->fn->peerdata = datum->next; + free(datum->id); + free(datum); +} + +void fnetpeerunset(struct fnetpeer *peer, wchar_t *id) +{ + int i; + struct fnetpeerdatum *datum; + + if((datum = finddatum(peer->fn, id)) == NULL) + return; + for(i = 0; i < peer->dinum; i++) + { + if(peer->peerdi[i].datum == datum) + break; + } + if(i >= peer->dinum) + return; + if((datum->datatype == FNPD_STR) && (peer->peerdi[i].data.str != NULL)) + free(peer->peerdi[i].data.str); + peer->dinum--; + memmove(&peer->peerdi[i], &peer->peerdi[i + 1], sizeof(struct fnetpeerdi) * (peer->dinum - i)); + putdatum(peer, datum); +} + +struct fnetpeer *fnetaddpeer(struct fnetnode *fn, wchar_t *id, wchar_t *nick) +{ + struct fnetpeer *new; + + new = smalloc(sizeof(*new)); + new->fn = fn; + new->id = swcsdup(id); + new->nick = swcsdup(nick); + new->flags.w = 0; + new->dinum = 0; + new->peerdi = NULL; + new->next = fn->peers; + new->prev = NULL; + if(fn->peers != NULL) + fn->peers->prev = new; + fn->peers = new; + fn->numpeers++; + CBCHAINDOCB(fn, fnetnode_ac, fn, L"numpeers"); + return(new); +} + +void fnetdelpeer(struct fnetpeer *peer) +{ + int i; + + if(peer->next != NULL) + peer->next->prev = peer->prev; + if(peer->prev != NULL) + peer->prev->next = peer->next; + if(peer->fn->peers == peer) + peer->fn->peers = peer->next; + peer->fn->numpeers--; + CBCHAINDOCB(peer->fn, fnetnode_ac, peer->fn, L"numpeers"); + free(peer->id); + free(peer->nick); + for(i = 0; i < peer->dinum; i++) + { + if((peer->peerdi[i].datum->datatype == FNPD_STR) && (peer->peerdi[i].data.str != NULL)) + free(peer->peerdi[i].data.str); + putdatum(peer, peer->peerdi[i].datum); + } + if(peer->peerdi != NULL) + free(peer->peerdi); + free(peer); +} + +struct fnetpeer *fnetfindpeer(struct fnetnode *fn, wchar_t *id) +{ + struct fnetpeer *cur; + + for(cur = fn->peers; (cur != NULL) && wcscmp(cur->id, id); cur = cur->next); + return(cur); +} + +int fnetsetnick(struct fnetnode *fn, wchar_t *newnick) +{ + int ret; + + if(fn->fnet->setnick != NULL) + ret = fn->fnet->setnick(fn, newnick); + else + ret = 0; + if(!ret) + { + if(fn->mynick != NULL) + free(fn->mynick); + fn->mynick = swcsdup(newnick); + } + return(ret); +} + +int fnetsendchat(struct fnetnode *fn, int public, wchar_t *to, wchar_t *string) +{ + if(fn->fnet->sendchat == NULL) + { + errno = ENOTSUP; + return(-1); + } + return(fn->fnet->sendchat(fn, public, to, string)); +} + +int fnetsearch(struct fnetnode *fn, struct search *srch, struct srchfnnlist *ln) +{ + if(fn->fnet->search == NULL) + { + errno = ENOTSUP; + return(-1); + } + return(fn->fnet->search(fn, srch, ln)); +} + +void fnetsetname(struct fnetnode *fn, wchar_t *newname) +{ + if(fn->name != NULL) + free(fn->name); + fn->name = swcsdup(newname); + CBCHAINDOCB(fn, fnetnode_ac, fn, L"name"); +} + +void fnetsetstate(struct fnetnode *fn, int newstate) +{ + fn->state = newstate; + CBCHAINDOCB(fn, fnetnode_ac, fn, L"state"); +} + +struct fnet *findfnet(wchar_t *name) +{ + struct fnet *fnet; + + for(fnet = networks; fnet != NULL; fnet = fnet->next) + { + if(!wcscmp(name, fnet->name)) + break; + } + return(fnet); +} + +struct fnetnode *fnetinitconnect(wchar_t *name, char *addr) +{ + struct fnet *fnet; + struct fnetnode *fn; + + if((fnet = findfnet(name)) == NULL) + { + errno = EPROTONOSUPPORT; + return(NULL); + } + fn = newfn(fnet); + getfnetnode(fn); + if(netresolve(addr, (void (*)(struct sockaddr *, int, void *))resolvecb, fn) < 0) + return(NULL); + return(fn); +} + +void regfnet(struct fnet *fnet) +{ + fnet->next = networks; + networks = fnet; +} + +/* + * Note on the chat string: Must be in UNIX text file format - that + * is, LF line endings. The filenet-specific code must see to it that + * any other kind of format is converted into that. In the future, + * certain control characters and escape sequences will be parsed by + * the client. Make sure that any filenet-specific code strips any + * such that aren't supposed to be in the protocol. + * + * Note on "name": This is supposed to be an identifier for the + * source. If the chat is a public message, set "public" to non-zero + * and "name" to whatever "chat room" name is appropriate for the + * fnetnode, but not NULL. If there is a "default" channel in this + * filenet, set "name" to the empty string. If the chat is a private + * message, name is ignored. + */ +void fnethandlechat(struct fnetnode *fn, int public, wchar_t *name, wchar_t *peer, wchar_t *chat) +{ + CBCHAINDOCB(fn, fnetnode_chat, fn, public, name, peer, chat); +} + +static struct configvar myvars[] = +{ + {CONF_VAR_INT, "srchwait", {.num = 15}}, + {CONF_VAR_INT, "fntos", {.num = 0}}, + {CONF_VAR_INT, "fnptos", {.num = 0}}, + {CONF_VAR_END} +}; + +static struct module me = +{ + .conf = + { + .vars = myvars + }, + .name = "fnet" +}; + +MODULE(me) diff --git a/daemon/filenet.h b/daemon/filenet.h new file mode 100644 index 0000000..8e3da7b --- /dev/null +++ b/daemon/filenet.h @@ -0,0 +1,138 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _FILENET_H +#define _FILENET_H + +#include +#include "net.h" +#include "utils.h" +#include "search.h" + +#define FNN_SYN 0 +#define FNN_HS 1 +#define FNN_EST 2 +#define FNN_DEAD 3 + +#define FNPD_INT 0 +#define FNPD_LL 1 +#define FNPD_STR 2 + +struct fnetnode; +struct fnetpeer; + +struct fnet +{ + struct fnet *next; + wchar_t *name; + void (*connect)(struct fnetnode *fn); + void (*destroy)(struct fnetnode *fn); + int (*setnick)(struct fnetnode *fn, wchar_t *newnick); + int (*reqconn)(struct fnetpeer *peer); + int (*sendchat)(struct fnetnode *fn, int public, wchar_t *to, wchar_t *string); + int (*search)(struct fnetnode *fn, struct search *srch, struct srchfnnlist *ln); + wchar_t *(*filebasename)(wchar_t *filename); +}; + +struct fnetpeerdatum +{ + struct fnetpeerdatum *next, *prev; + int refcount; + wchar_t *id; + int datatype; +}; + +struct fnetpeerdi +{ + struct fnetpeerdatum *datum; + union + { + int num; + long long lnum; + wchar_t *str; + } data; +}; + +struct fnetpeer +{ + struct fnetpeer *next, *prev; + struct fnetnode *fn; + wchar_t *id; + wchar_t *nick; + union + { + struct + { + int delete:1; + int op:1; + } b; + int w; + } flags; + int dinum; + struct fnetpeerdi *peerdi; +}; + +struct fnetnode +{ + struct fnetnode *next, *prev; + int refcount; + int id; + int state; + int linked; + time_t srchwait, lastsrch; + wchar_t *name; + wchar_t *mynick; + struct fnet *fnet; + struct socket *sk; + struct fnetpeerdatum *peerdata; + struct fnetpeer *peers; + CBCHAIN(fnetnode_ac, struct fnetnode *fn, wchar_t *attrib); + CBCHAIN(fnetnode_chat, struct fnetnode *fn, int public, wchar_t *name, wchar_t *peer, wchar_t *string); + CBCHAIN(fnetnode_unlink, struct fnetnode *fn); + CBCHAIN(fnetnode_destroy, struct fnetnode *fn); + int numpeers; + void *data; +}; + +void regfnet(struct fnet *fnet); +void fnetsetname(struct fnetnode *fn, wchar_t *newname); +void fnetsetstate(struct fnetnode *fn, int newstate); +int fnetsetnick(struct fnetnode *fn, wchar_t *newnick); +struct fnet *findfnet(wchar_t *name); +struct fnetnode *fnetinitconnect(wchar_t *name, char *addr); +void linkfnetnode(struct fnetnode *fn); +void unlinkfnetnode(struct fnetnode *fn); +void getfnetnode(struct fnetnode *fn); +void putfnetnode(struct fnetnode *fn); +void killfnetnode(struct fnetnode *fn); +struct fnetpeer *fnetaddpeer(struct fnetnode *fn, wchar_t *id, wchar_t *nick); +void fnetdelpeer(struct fnetpeer *peer); +struct fnetpeer *fnetfindpeer(struct fnetnode *fn, wchar_t *id); +void fnetpeersetstr(struct fnetpeer *peer, wchar_t *id, wchar_t *value); +void fnetpeersetnum(struct fnetpeer *peer, wchar_t *id, int value); +void fnetpeersetlnum(struct fnetpeer *peer, wchar_t *id, long long value); +void fnetpeerunset(struct fnetpeer *peer, wchar_t *id); +struct fnetnode *findfnetnode(int id); +void fnethandlechat(struct fnetnode *fn, int public, wchar_t *name, wchar_t *peer, wchar_t *chat); +int fnetsendchat(struct fnetnode *fn, int public, wchar_t *to, wchar_t *string); +int fnetsearch(struct fnetnode *fn, struct search *srch, struct srchfnnlist *ln); + +extern struct fnetnode *fnetnodes; +EGCBCHAIN(newfncb, struct fnetnode *); + +#endif diff --git a/daemon/fnet-dc.c b/daemon/fnet-dc.c new file mode 100644 index 0000000..4afd0c0 --- /dev/null +++ b/daemon/fnet-dc.c @@ -0,0 +1,3302 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "filenet.h" +#include "log.h" +#include "module.h" +#include "utils.h" +#include "client.h" +#include "transfer.h" +#include "sysevents.h" +#include "net.h" + +/* + * The Direct Connect protocol is extremely ugly. Thus, this code must + * also be a bit ugly in certain places. Please forgive me. =/ + * + * This also means that there might be some pieces of the code that + * look completely illogical to you, and you think that you may make + * them much neater/better. However, in many cases it might just be + * that it's required to cope with some oddity of the protocol, such + * as the amazingly ugly delayed key response in the peer protocol. + */ + +/* I assume this is the correct character set to use for DC, + * considering it was developed without i18n support under Windows */ +#define DCCHARSET "windows-1252" + +#ifdef DCPP_MASQUERADE +/* + * I honestly don't want to pretend being a client that I'm not, but + * there are so many hubs that simply do not accept any clients + * outside their whitelists, for no obvious reasons, so I feel that I + * am left with little choice. Anyhow, as long as I actually support + * all the features that my faked DC++ version does, there should be + * very little harm done. + */ +#define DCIDTAG "++" +#define DCIDTAGV "0.674" +#define DCIDFULL "DC++ 0.674" +#else +#define DCIDTAG "Dolda" +#define DCIDTAGV VERSION +#define DCIDFULL "DoldaConnect " VERSION +#endif + +#define PEER_CMD 0 +#define PEER_TRNS 1 +#define PEER_SYNC 2 + +#define CPRS_NONE 0 +#define CPRS_ZLIB 1 + +struct command +{ + char *name; + void (*handler)(struct socket *sk, void *data, char *cmd, char *args); +}; + +struct qcommand +{ + struct qcommand *next; + char *string; +}; + +struct dchub +{ + char *inbuf; + size_t inbufdata, inbufsize; + struct qcommand *queue; + int extended; + char *nativename; + char *nativenick; +}; + +struct dcexppeer +{ + struct dcexppeer *next, *prev; + char *nick; + struct fnetnode *fn; + struct timer *expire; +}; + +struct dcpeer +{ + struct dcpeer *next, *prev; + struct socket *sk; + struct fnetnode *fn; + char *inbuf; + size_t inbufdata, inbufsize; + int freeing; + struct qcommand *queue; + struct transfer *transfer; + int state; + int ptclose; /* Close after transfer is complete */ + int accepted; /* If false, we connected, otherwise, we accepted */ + int extended; + int direction; /* Using the constants from transfer.h */ + int compress; + void *cprsdata; + char *mbspath; + char *key; + char *nativename; + char **supports; + wchar_t *wcsname; +}; + +static struct fnet dcnet; +static struct transferiface dctransfer; +static struct socket *udpsock = NULL; +static struct socket *tcpsock = NULL; +static struct dcpeer *peers = NULL; +int numdcpeers = 0; +static struct dcexppeer *expected = NULL; +static char *hmlistname = NULL; +static char *xmllistname = NULL; +static char *xmlbz2listname = NULL; + +static void peerconnect(struct socket *sk, int err, struct fnetnode *fn); +static void freedcpeer(struct dcpeer *peer); +static void transread(struct socket *sk, struct dcpeer *peer); +static void transerr(struct socket *sk, int err, struct dcpeer *peer); +static void transwrite(struct socket *sk, struct dcpeer *peer); +static void updatehmlist(void); +static void updatexmllist(void); +static void updatexmlbz2list(void); + +static int reservedchar(unsigned char c) +{ + return((c == 0) || (c == 5) || (c == 124) || (c == 96) || (c == 126) || (c == 36)); +} + +/* Oh, how I despise having to do this... */ +static char *dcmakekey(char *lock) +{ + int i, len, offset; + char *buf, *key; + char save; + + buf = smalloc(strlen(lock)); + save = 5; + len = 0; + for(i = 0; lock[i]; i++) + { + buf[i] = lock[i] ^ save; + buf[i] = ((buf[i] & 0x0F) << 4) | ((buf[i] & 0xF0) >> 4); + save = lock[i]; + if((i != 0) && reservedchar(buf[i])) + len += 10; + else + len++; + } + buf[0] ^= buf[i - 1]; + if(reservedchar(buf[0])) + len += 10; + else + len++; + key = smalloc(len + 1); + offset = 0; + for(i = 0; lock[i] != 0; i++) + { + if(reservedchar(buf[i])) + offset += sprintf(key + offset, "/%%DCN%03i%%/", buf[i]); + else + key[offset++] = buf[i]; + } + key[offset] = 0; + free(buf); + return(key); +} + +static void endcompress(struct dcpeer *peer) +{ + if(peer->compress == CPRS_ZLIB) + { + deflateEnd(peer->cprsdata); + free(peer->cprsdata); + } + peer->compress = CPRS_NONE; +} + +static void initcompress(struct dcpeer *peer, int algo) +{ + int ret; + + endcompress(peer); + peer->compress = algo; + if(algo == CPRS_ZLIB) + { + peer->cprsdata = smalloc(sizeof(z_stream)); + memset(peer->cprsdata, 0, sizeof(z_stream)); + if((ret = deflateInit(peer->cprsdata, 3)) != Z_OK) + { + flog(LOG_CRIT, "Aiya! zlib refuses to init (%i)!", ret); + abort(); + } + } +} + +static void unquote(wchar_t *in) +{ + wchar_t *p, *p2, nc; + + for(p = in; *p != L'\0'; p++) + { + if(*p == L'&') + { + for(p2 = p + 1; (*p2 != L'\0') && (*p2 != L';') && (*p2 != L'&'); p2++); + if(*p2 == L'&') + continue; + if(*p2 == L'\0') + return; + *p2 = L'\0'; + nc = L'\0'; + if(!wcscmp(p + 1, L"amp")) + { + nc = L'&'; + } else if(p[1] == L'#') { + nc = ucptowc(wcstol(p + 2, NULL, 10)); + } + if(nc == L'\0') + { + *p2 = L';'; + p = p2; + continue; + } + *p = nc; + memmove(p + 1, p2 + 1, (wcslen(p2 + 1) + 1) * sizeof(wchar_t)); + } + } +} + +static void freeexppeer(struct dcexppeer *ep) +{ + if(ep->next != NULL) + ep->next->prev = ep->prev; + if(ep->prev != NULL) + ep->prev->next = ep->next; + if(ep == expected) + expected = ep->next; + free(ep->nick); + putfnetnode(ep->fn); + if(ep->expire != NULL) + canceltimer(ep->expire); + free(ep); +} + +static void exppeerexpire(int cancelled, struct dcexppeer *ep) +{ + ep->expire = NULL; + if(!cancelled) + freeexppeer(ep); +} + +static struct dcexppeer *expectpeer(char *nick, struct fnetnode *fn) +{ + struct dcexppeer *ep; + + ep = smalloc(sizeof(*ep)); + ep->nick = sstrdup(nick); + getfnetnode(ep->fn = fn); + ep->expire = timercallback(ntime() + 300, (void (*)(int, void *))exppeerexpire, ep); + ep->next = expected; + ep->prev = NULL; + if(expected != NULL) + expected->prev = ep; + expected = ep; + return(ep); +} + +static struct qcommand *newqcmd(struct qcommand **queue, char *string) +{ + struct qcommand *new; + + while(*queue != NULL) + queue = &(*queue)->next; + new = smalloc(sizeof(*new)); + new->string = sstrdup(string); + new->next = *queue; + *queue = new; + return(new); +} + +static struct qcommand *ulqcmd(struct qcommand **queue) +{ + struct qcommand *qcmd; + + if((qcmd = *queue) == NULL) + return(NULL); + *queue = qcmd->next; + return(qcmd); +} + +static void freeqcmd(struct qcommand *qcmd) +{ + free(qcmd->string); + free(qcmd); +} + +static void hubrecvchat(struct socket *sk, struct fnetnode *fn, char *from, char *string) +{ + wchar_t *chat, *wfrom, *wpeer; + char *p, *end; + struct fnetpeer *peer; + + end = string + strlen(string); + while((p = strchr(string, 13)) != NULL) + memmove(p, p + 1, (end-- - p)); + if(from != NULL) + { + if((strlen(string) > strlen(from) + 2) && (*string == '<') && !memcmp(string + 1, from, strlen(from)) && (*(string + strlen(from) + 1) == '>')) + string += strlen(from) + 2; + if((wfrom = icmbstowcs(from, DCCHARSET)) == NULL) + return; + wpeer = swcsdup(wfrom); + } else { + wfrom = NULL; + wpeer = NULL; + if(*string == '<') + { + for(p = string + 1; *p; p++) + { + if((*p == ' ') || (*p == '>')) + break; + } + if(*p == '>') + { + *(p++) = 0; + if(*p == ' ') + p++; + if((wpeer = icmbstowcs(string + 1, DCCHARSET)) == NULL) + return; + string = p; + } + } + if(wpeer == NULL) + wpeer = swcsdup(L""); + } + if((chat = icmbstowcs(string, DCCHARSET)) == NULL) + { + if(wfrom != NULL) + free(wfrom); + free(wpeer); + return; + } + unquote(chat); + if(wfrom != NULL) + { + if((peer = fnetfindpeer(fn, wfrom)) == NULL) /* Assume public chat */ + fnethandlechat(fn, 1, wfrom, wpeer, chat); + else + fnethandlechat(fn, 0, wfrom, wpeer, chat); + } else { + fnethandlechat(fn, 1, L"", wpeer, chat); + } + if(wfrom != NULL) + free(wfrom); + free(wpeer); + free(chat); +} + +static void sendadc(struct socket *sk, char *arg) +{ + char *buf; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + addtobuf(buf, ' '); + for(; *arg; arg++) + { + if(*arg == ' ') + { + bufcat(buf, "\\s", 2); + } else if(*arg == '\n') { + bufcat(buf, "\\n", 2); + } else if(*arg == '\\') { + bufcat(buf, "\\\\", 2); + } else { + addtobuf(buf, *arg); + } + } + sockqueue(sk, buf, bufdata); + free(buf); +} + +static void sendadcf(struct socket *sk, char *arg, ...) +{ + char *buf; + va_list args; + + va_start(args, arg); + buf = vsprintf2(arg, args); + va_end(args); + if(buf == NULL) + return; + sendadc(sk, buf); + free(buf); +} + +static char **parseadc(char *args) +{ + char **retbuf; + size_t retbufsize, retbufdata; + char *buf; + size_t bufsize, bufdata; + int state; + + retbuf = NULL; + buf = NULL; + retbufsize = retbufdata = bufsize = bufdata = 0; + state = 0; + while(state != 3) + { + switch(state) + { + case 0: + if(*args == 0) + state = 3; + else if(*args != ' ') + state = 1; + break; + case 1: + if((*args == ' ') || (*args == 0)) + { + addtobuf(buf, 0); + addtobuf(retbuf, buf); + buf = NULL; + bufsize = bufdata = 0; + if(*args == 0) + state = 3; + } else if(*args == '\\') { + state = 2; + } else { + addtobuf(buf, *args); + } + args++; + break; + case 2: + if(*args == 0) + { + if(buf != NULL) + free(buf); + addtobuf(retbuf, NULL); + freeparr(retbuf); + return(NULL); + } else if(*args == 's') { + addtobuf(buf, ' '); + } else if(*args == 'n') { + addtobuf(buf, '\n'); + } else if(*args == '\\') { + addtobuf(buf, '\\'); + } + state = 1; + } + } + if(buf != NULL) + free(buf); + addtobuf(retbuf, NULL); + return(retbuf); +} + +/* Macros useful in command handlers */ +#define skipspace(s) ({if(((s) = strchr((s), ' ')) == NULL) return; else (s)++;}) +#define qstr(sk, str) sockqueue(sk, str, strlen(str)) +#define qstrf(sk, strandargs...) \ +do { \ + char *__buf__; \ + if((__buf__ = sprintf2(strandargs)) != NULL) { \ + sockqueue(sk, __buf__, strlen(__buf__)); \ + free(__buf__); \ + } \ +} while(0) + +static char *tr(char *str, char *trans) +{ + char *p; + + for(; *trans; trans += 2) + { + for(p = strchr(str, trans[0]); p != NULL; p = strchr(p, trans[0])) + *p = trans[1]; + } + return(str); +} + +static int trresumecb(struct transfer *transfer, wchar_t *cmd, wchar_t *arg, struct dcpeer *peer) +{ + if(!wcscmp(cmd, L"resume")) + { + if(arg == NULL) + { + flog(LOG_WARNING, "filter returned no position for \"resume\" on transfer %i", transfer->id); + freedcpeer(peer); + } else { + transfer->curpos = wcstol(arg, NULL, 10); + qstrf(peer->sk, "$Get %s$%i|", peer->mbspath, transfer->curpos + 1); + } + free(peer->mbspath); + peer->mbspath = NULL; + return(1); + } + return(0); +} + +static void peerhandleaction(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + struct dchub *hub; + struct dcexppeer *expect; + struct transfer *transfer; + wchar_t tbuf[128]; + char *mbsbuf; + + hub = NULL; + if(peer->fn != NULL) + { + if(peer->fn->fnet != &dcnet) + { + peer->sk->close = 1; + return; + } + hub = peer->fn->data; + } + if(peer->transfer != NULL) + { + swprintf(tbuf, 128, L"hs: dc-%s", cmd); + transfersetactivity(peer->transfer, tbuf); + } + if(peer->accepted) + { + if(cmd == NULL) /* Connect event */ + { + } else if(!strcmp(cmd, "$MyNick")) { + for(expect = expected; expect != NULL; expect = expect->next) + { + if(!strcmp(expect->nick, args)) + break; + } + if(expect == NULL) + { + peer->fn = NULL; + } else { + peer->fn = expect->fn; + getfnetnode(peer->fn); + freeexppeer(expect); + } + } else if(!strcmp(cmd, "$Lock")) { + if(peer->wcsname == NULL) + { + freedcpeer(peer); + return; + } + if(hub == NULL) + qstrf(sk, "$MyNick %s|", icswcstombs(confgetstr("cli", "defnick"), DCCHARSET, "DoldaConnectUser-IN")); + else + qstrf(sk, "$MyNick %s|", hub->nativenick); +#ifdef DCPP_MASQUERADE + qstrf(sk, "$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.674ABCABC|"); +#else + qstrf(sk, "$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DOLDA%sABCABCABC|", VERSION); +#endif + if(peer->extended) + { +#ifdef DCPP_MASQUERADE + qstr(sk, "$Supports MiniSlots XmlBZList ADCGet TTHL TTHF GetZBlock ZLIG |"); +#else + qstr(sk, "$Supports MiniSlots XmlBZList ADCGet TTHL TTHF GetZBlock ZLIG|"); +#endif + } + for(transfer = transfers; transfer != NULL; transfer = transfer->next) + { + if((transfer->dir == TRNSD_DOWN) && (transfer->iface == NULL) && !wcscmp(peer->wcsname, transfer->peerid)) + break; + } + if(transfer == NULL) + { + peer->direction = TRNSD_UP; + transfer = newupload(peer->fn, &dcnet, peer->wcsname, &dctransfer, peer); + transfersetnick(transfer, peer->wcsname); + peer->transfer = transfer; + } else { + peer->direction = TRNSD_DOWN; + peer->transfer = transfer; + transferattach(transfer, &dctransfer, peer); + transfersetnick(transfer, peer->wcsname); + transfersetstate(transfer, TRNS_HS); + } + qstrf(sk, "$Direction %s %i|", (peer->direction == TRNSD_UP)?"Upload":"Download", rand() % 10000); + if(peer->key != NULL) + { + /* I hate the DC protocol so much... */ + qstrf(sk, "$Key %s|", peer->key); + free(peer->key); + peer->key = NULL; + } + if(peer->direction == TRNSD_DOWN) + { + if((mbsbuf = icwcstombs(peer->transfer->path, DCCHARSET)) == NULL) + { + /* I believe that NOTFOUND should be used + * since giving a path that cannot be + * represented in the protocol's charset is + * literally the same as giving a path that + * the client doesn't have. */ + transferseterror(peer->transfer, TRNSE_NOTFOUND); + freedcpeer(peer); + return; + } + if(peer->transfer->size == -1) + { + /* The transfer will be restarted later from + * cmd_filelength when it detects that the sizes + * don't match. */ + qstrf(sk, "$Get %s$1|", mbsbuf); + } else { + if(forkfilter(transfer)) + { + flog(LOG_WARNING, "could not fork filter for transfer %i: %s", transfer->id, strerror(errno)); + freedcpeer(peer); + free(mbsbuf); + return; + } + peer->mbspath = sstrdup(mbsbuf); + CBREG(transfer, trans_filterout, (int (*)(struct transfer *, wchar_t *, wchar_t *, void *))trresumecb, NULL, peer); + } + free(mbsbuf); + } + } else if(!strcmp(cmd, "$FileLength")) { + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + transfersetstate(peer->transfer, TRNS_MAIN); + socksettos(peer->sk, confgetint("transfer", "dltos")); + peer->state = PEER_TRNS; + peer->sk->readcb = (void (*)(struct socket *, void *))transread; + peer->sk->errcb = (void (*)(struct socket *, int, void *))transerr; + qstr(peer->sk, "$Send|"); + } + } else { + if(cmd == NULL) /* Connect event */ + { + if(hub == NULL) + qstrf(sk, "$MyNick %s|", icswcstombs(confgetstr("cli", "defnick"), DCCHARSET, "DoldaConnectUser-IN")); + else + qstrf(sk, "$MyNick %s|", hub->nativenick); +#ifdef DCPP_MASQUERADE + qstrf(sk, "$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.674ABCABC|"); +#else + qstrf(sk, "$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DOLDA%sABCABCABC|", VERSION); +#endif + } else if(!strcmp(cmd, "$Direction")) { + if(peer->wcsname == NULL) + { + freedcpeer(peer); + return; + } + if(peer->direction == TRNSD_UP) + { + transfer = newupload(peer->fn, &dcnet, peer->wcsname, &dctransfer, peer); + transfersetnick(transfer, peer->wcsname); + peer->transfer = transfer; + } else { + for(transfer = transfers; transfer != NULL; transfer = transfer->next) + { + if((transfer->dir == TRNSD_DOWN) && (transfer->state == TRNS_WAITING) && !wcscmp(peer->wcsname, transfer->peerid)) + break; + } + if(transfer == NULL) + { + freedcpeer(peer); + return; + } + peer->transfer = transfer; + transferattach(transfer, &dctransfer, peer); + transfersetnick(transfer, peer->wcsname); + transfersetstate(transfer, TRNS_HS); + } + if(peer->extended) + { +#ifdef DCPP_MASQUERADE + qstr(sk, "$Supports MiniSlots XmlBZList ADCGet TTHL TTHF GetZBlock ZLIG |"); +#else + qstr(sk, "$Supports MiniSlots XmlBZList ADCGet TTHL TTHF GetZBlock ZLIG|"); +#endif + } + qstrf(sk, "$Direction %s %i|", (peer->direction == TRNSD_UP)?"Upload":"Download", rand() % 10000); + if(peer->key != NULL) + qstrf(sk, "$Key %s|", peer->key); + if(peer->direction == TRNSD_DOWN) + { + if((mbsbuf = icwcstombs(peer->transfer->path, DCCHARSET)) == NULL) + { + /* I believe that NOTFOUND should be used + * since giving a path that cannot be + * represented in the protocol's charset is + * literally the same as giving a path that + * the client doesn't have. */ + transferseterror(peer->transfer, TRNSE_NOTFOUND); + freedcpeer(peer); + return; + } + if(peer->transfer->size == -1) + { + /* The transfer will be restarted later from + * cmd_filelength when it detects that the sizes + * don't match. */ + qstrf(sk, "$Get %s$1|", mbsbuf); + } else { + if(forkfilter(transfer)) + { + flog(LOG_WARNING, "could not fork filter for transfer %i: %s", transfer->id, strerror(errno)); + freedcpeer(peer); + free(mbsbuf); + return; + } + peer->mbspath = sstrdup(mbsbuf); + CBREG(transfer, trans_filterout, (int (*)(struct transfer *, wchar_t *, wchar_t *, void *))trresumecb, NULL, peer); + } + free(mbsbuf); + } + } else if(!strcmp(cmd, "$FileLength")) { + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + transfersetstate(peer->transfer, TRNS_MAIN); + socksettos(peer->sk, confgetint("transfer", "dltos")); + peer->state = PEER_TRNS; + peer->sk->readcb = (void (*)(struct socket *, void *))transread; + peer->sk->errcb = (void (*)(struct socket *, int, void *))transerr; + qstr(peer->sk, "$Send|"); + } + } +} + +static void sendmyinfo(struct socket *sk, struct fnetnode *fn) +{ + struct dchub *hub; + char *buf; + struct fnetnode *cfn; + int numhubs; + + hub = fn->data; + qstrf(sk, "$MyINFO $ALL %s ", hub->nativenick); + buf = tr(icswcstombs(confgetstr("dc", "desc"), DCCHARSET, "Charset_conv_failure"), "$_|_"); + qstrf(sk, "%s", buf); + numhubs = 0; + for(cfn = fnetnodes; cfn != NULL; cfn = cfn->next) + { + if((cfn->state == FNN_EST) || (cfn->state == FNN_HS)) + numhubs++; + } + qstrf(sk, "<%s V:%s,M:%c,H:%i/0/0,S:%i>", + DCIDTAG, + DCIDTAGV, + (tcpsock == NULL)?'P':'A', + numhubs, + confgetint("transfer", "slots") + ); + qstrf(sk, "$ $"); + buf = tr(icswcstombs(confgetstr("dc", "speedstring"), DCCHARSET, "Charset_conv_failure"), "$_|_"); + qstrf(sk, "%s\x01$", buf); + buf = tr(icswcstombs(confgetstr("dc", "email"), DCCHARSET, "Charset_conv_failure"), "$_|_"); + qstrf(sk, "%s$", buf); + qstrf(sk, "%llu$|", sharesize); +} + +static void hubhandleaction(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + + hub = fn->data; + if(!strcmp(cmd, "$Lock")) + { + qstrf(sk, "$ValidateNick %s|", hub->nativenick); + } else if(!strcmp(cmd, "$Hello")) { + if(fn->state == FNN_HS) + { + qstrf(sk, "$Version 1,0091|"); + qstrf(sk, "$GetNickList|"); + sendmyinfo(sk, fn); + fnetsetstate(fn, FNN_EST); + } else { + qstrf(sk, "$GetINFO %s %s|", args, hub->nativenick); + } + } +} + +static void cmd_lock(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + char *key; + char *p; + + hub = fn->data; + if(!strncmp(args, "EXTENDEDPROTOCOL", 16)) + hub->extended = 1; + if((p = strchr(args, ' ')) != NULL) + *(p++) = 0; + if(hub->extended) + { +#ifdef DCPP_MASQUERADE + qstrf(sk, "$Supports UserCommand NoGetINFO NoHello UserIP2 TTHSearch GetZBlock |"); +#else + qstrf(sk, "$Supports UserCommand NoGetINFO NoHello UserIP2 TTHSearch GetZBlock|"); +#endif + } + key = dcmakekey(args); + qstrf(sk, "$Key %s|", key); + free(key); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_hubname(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + wchar_t *buf; + struct dchub *hub; + + hub = fn->data; + if(hub->nativename == NULL) + free(hub->nativename); + hub->nativename = sstrdup(args); + buf = icmbstowcs(args, DCCHARSET); + fnetsetname(fn, (buf == NULL)?L"Hubname conv error":buf); + if(buf != NULL) + free(buf); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_hello(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + wchar_t *nick; + struct dchub *hub; + + hub = fn->data; + if((nick = icmbstowcs(args, DCCHARSET)) == NULL) + return; + if(strcmp(args, hub->nativenick) && (fnetfindpeer(fn, nick) == NULL)) + fnetaddpeer(fn, nick, nick); + free(nick); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_quit(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + wchar_t *nick; + struct fnetpeer *peer; + struct dchub *hub; + + hub = fn->data; + if((nick = icmbstowcs(args, DCCHARSET)) == NULL) + return; + if((peer = fnetfindpeer(fn, nick)) != NULL) + fnetdelpeer(peer); + free(nick); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_nicklist(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + char *p; + wchar_t *buf; + struct fnetpeer *peer, *npeer; + + hub = fn->data; + for(peer = fn->peers; peer != NULL; peer = peer->next) + peer->flags.b.delete = 1; + while((p = strstr(args, "$$")) != NULL) + { + *p = 0; + if((buf = icmbstowcs(args, DCCHARSET)) != NULL) + { + if((peer = fnetfindpeer(fn, buf)) == NULL) + peer = fnetaddpeer(fn, buf, buf); + else + peer->flags.b.delete = 0; + free(buf); + qstrf(sk, "$GetINFO %s %s|", args, hub->nativenick); + } + args = p + 2; + } + for(peer = fn->peers; peer != NULL; peer = npeer) + { + npeer = peer->next; + if(peer->flags.b.delete) + fnetdelpeer(peer); + } + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_oplist(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + char *p; + wchar_t *buf; + struct fnetpeer *peer; + + hub = fn->data; + for(peer = fn->peers; peer != NULL; peer = peer->next) + peer->flags.b.op = 0; + while((p = strstr(args, "$$")) != NULL) + { + *p = 0; + if((buf = icmbstowcs(args, DCCHARSET)) != NULL) + { + if((peer = fnetfindpeer(fn, buf)) != NULL) + peer->flags.b.op = 1; + free(buf); + } + args = p + 2; + } + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_myinfo(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + char *p, *p2; + wchar_t *buf, *wp, *wp2; + wchar_t abuf[10]; + struct fnetpeer *peer; + struct dchub *hub; + + hub = fn->data; + p = args; + if(strncmp(p, "$ALL ", 5)) + return; + p += 5; + if((p2 = strchr(p, ' ')) == NULL) + return; + *p2 = 0; + if((buf = icmbstowcs(p, DCCHARSET)) == NULL) + return; + if((peer = fnetfindpeer(fn, buf)) == NULL) + peer = fnetaddpeer(fn, buf, buf); + free(buf); + p = p2 + 1; + if((p2 = strstr(p, "$ $")) == NULL) + return; + *p2 = 0; + if((buf = icmbstowcs(p, DCCHARSET)) == NULL) + return; + if((wcslen(buf) > 0) && (buf[wcslen(buf) - 1] == L'>') && ((wp = wcschr(buf, L'<')) != NULL)) + { + buf[wcslen(buf) - 1] = L'\0'; + *(wp++) = L'\0'; + if((wp2 = wcschr(wp, L' ')) != NULL) + { + *(wp2++) = L'\0'; + fnetpeersetstr(peer, L"dc-client", wp); + wp = wp2; + do + { + if((wp2 = wcschr(wp, L',')) != NULL) + *(wp2++) = L'\0'; + if(wp[1] != L':') + continue; + swprintf(abuf, 10, L"dc-tag-%lc", wp[0]); + fnetpeersetstr(peer, abuf, wp + 2); + wp = wp2; + } while(wp2 != NULL); + } + } + fnetpeersetstr(peer, L"descr", buf); + free(buf); + p = p2 + 3; + if((p2 = strchr(p, '$')) == NULL) + return; + *(p2 - 1) = 0; + if((buf = icmbstowcs(p, DCCHARSET)) == NULL) + return; + fnetpeersetstr(peer, L"dc-speed", buf); + free(buf); + p = p2 + 1; + if((p2 = strchr(p, '$')) == NULL) + return; + *p2 = 0; + if((buf = icmbstowcs(p, DCCHARSET)) == NULL) + return; + fnetpeersetstr(peer, L"email", buf); + free(buf); + p = p2 + 1; + if(strlen(p) < 1) + return; + fnetpeersetlnum(peer, L"share", strtoll(p, NULL, 10)); + hubhandleaction(sk, fn, cmd, args); +} + +/* I do not implement the fully in that I do not disconnect from the + * old hub. I do this since I believe that if the hub owner really + * wants to get rid of us, then it is his responsibility to disconnect + * us. */ +static void cmd_forcemove(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + struct fnetnode *newfn; + int freeargs; + + hub = fn->data; + if(strchr(args, ':') == NULL) + { + args = strcpy(smalloc(strlen(args) + 5), args); + strcat(args, ":411"); + freeargs = 1; + } else { + freeargs = 0; + } + if((newfn = fnetinitconnect(L"dc", args)) != NULL) + { + linkfnetnode(newfn); + putfnetnode(newfn); + } + hubhandleaction(sk, fn, cmd, args); + if(freeargs) + free(args); +} + +static char *getdcpath(struct sharecache *node, size_t *retlen) +{ + char *buf, *buf2; + size_t len, len2; + + if(node->parent == NULL) + return(NULL); + if(node->parent == shareroot) + { + if((buf = icwcstombs(node->name, DCCHARSET)) == NULL) + return(NULL); + if(retlen != NULL) + *retlen = strlen(buf); + return(buf); + } else { + if((buf2 = icwcstombs(node->name, DCCHARSET)) == NULL) + return(NULL); + if((buf = getdcpath(node->parent, &len)) == NULL) + { + free(buf2); + return(NULL); + } + len2 = strlen(buf2); + buf = srealloc(buf, len + 1 + len2 + 1); + buf[len++] = '\\'; + strcpy(buf + len, buf2); + buf[len + len2] = 0; + free(buf2); + if(retlen != NULL) + *retlen = len + len2; + return(buf); + } +} + +/* + * This is the main share searching function for Direct Connect + * peers. Feel free to optimize it if you feel the need for it. I + * haven't ever seen it take any noticable CPU time anyway, so I + * haven't felt that need. + * + * It may feel dubious to just return the directory when all terms are + * satisfied rather than all the files in it, but it really benefits + * everyone: Less cycles used for us, less UDP squelching for active + * searchers, and less bandwidth waste for hubs when serving passive + * searchers. + */ +static void cmd_search(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + int i, done; + struct dchub *hub; + char *p, *p2; + char *prefix, *infix, *postfix, *buf, *buf2; + struct socket *dsk; + struct sockaddr_in addr; + struct sharecache *node; + int minsize, maxsize; + int dotth, buflen; + int termnum, satisfied, skipcheck; + int level, tersat[32]; + wchar_t *terms[32]; + char hashtth[24]; + + hub = fn->data; + if((p = strchr(args, ' ')) == NULL) + return; + *(p++) = 0; + + memset(terms, 0, sizeof(terms)); + prefix = infix = postfix = NULL; + dsk = NULL; + dotth = 0; + + if(!strncmp(args, "Hub:", 4)) + { + if(!strcmp(cmd, "$MultiSearch")) + goto out; + if(!strcmp(args + 4, hub->nativenick)) + goto out; + prefix = sprintf2("$SR %s ", hub->nativenick); + infix = sprintf2(" %i/%i\005", slotsleft(), confgetint("transfer", "slots")); + postfix = sprintf2(" (%s)\005%s|", formataddress(fn->sk->remote, fn->sk->remotelen), args + 4); + dsk = sk; + getsock(dsk); + } else { + if((p2 = strchr(args, ':')) == NULL) + goto out; + *(p2++) = 0; + addr.sin_family = AF_INET; + if(!inet_aton(args, &addr.sin_addr)) + goto out; + addr.sin_port = htons(atoi(p2)); + prefix = sprintf2("$SR %s ", hub->nativenick); + infix = sprintf2(" %i/%i\005", slotsleft(), confgetint("transfer", "slots")); + postfix = sprintf2(" (%s)|", formataddress(fn->sk->remote, fn->sk->remotelen)); + netdgramconn(dsk = netdupsock(udpsock), (struct sockaddr *)&addr, sizeof(addr)); + } + + minsize = maxsize = -1; + if(*p == 0) + goto out; + if(*(p++) == 'T') + minsize = 0; + if(*(p++) != '?') + goto out; + if(*p == 0) + goto out; + if(*(p++) == 'T') + { + maxsize = 0; + minsize = -1; + } + if(*(p++) != '?') + goto out; + if((p2 = strchr(p, '?')) == NULL) + goto out; + *(p2++) = 0; + if(minsize == 0) + minsize = atoi(p); + if(maxsize == 0) + maxsize = atoi(p); + p = p2 + 1; + if(*(p++) != '?') + goto out; + termnum = 0; + p2 = p; + done = 0; + while(!done) + { + if((*p2 == 0) || (*p2 == '$')) + { + if(*p2 == 0) + done = 1; + else + *p2 = 0; + if(*p) + { + if(!dotth && !strncmp(p, "TTH:", 4)) + { + dotth = 1; + if((buf = base32decode(p + 4, &buflen)) == NULL) + goto out; + if(buflen != 24) + goto out; + memcpy(hashtth, buf, 24); + free(buf); + } else { + if((terms[termnum] = icmbstowcs(p, DCCHARSET)) != NULL) + termnum++; + } + } + p = p2 + 1; + if(termnum == 32) + break; + } + p2++; + } + + node = shareroot->child; + level = 0; + for(i = 0; i < termnum; i++) + tersat[i] = -1; + satisfied = 0; + while(1) + { + skipcheck = 0; + if(node->f.b.type == FILE_REG) + { + if((minsize >= 0) && (node->size < minsize)) + skipcheck = 1; + if((maxsize >= 0) && (node->size > maxsize)) + skipcheck = 1; + } + if(!skipcheck && dotth) + { + if((node->f.b.type != FILE_REG) || (node->f.b.hastth && memcmp(hashtth, node->hashtth, 24))) + skipcheck = 1; + } + if(!skipcheck) + { + for(i = 0; i < termnum; i++) + { + if(tersat[i] >= 0) + continue; + if(wcsexists(node->name, terms[i])) + { + tersat[i] = level; + satisfied++; + } else if(node->child == NULL) { + break; + } + } + } + if(!skipcheck && (satisfied == termnum)) + { + if((buf = getdcpath(node, NULL)) != NULL) + { + if(node->f.b.hastth) + { + buf2 = base32encode(node->hashtth, 24); + qstrf(dsk, "%s%s\005%i%sTTH:%.39s%s", prefix, buf, node->size, infix, buf2, postfix); + free(buf2); + } else { + qstrf(dsk, "%s%s\005%i%s%s%s", prefix, buf, node->size, infix, hub->nativename, postfix); + } + free(buf); + } + } + if((!skipcheck && (satisfied == termnum)) || (node->child == NULL)) + { + while(node->next == NULL) + { + if((node = node->parent) == shareroot) + break; + level--; + } + if(node == shareroot) + break; + for(i = 0; i < termnum; i++) + { + if(tersat[i] >= level) + { + tersat[i] = -1; + satisfied--; + } + } + node = node->next; + } else { + node = node->child; + level++; + } + } + + hubhandleaction(sk, fn, cmd, args); + + out: + if(dsk != NULL) + putsock(dsk); + if(prefix != NULL) + free(prefix); + if(infix != NULL) + free(infix); + if(postfix != NULL) + free(postfix); + for(i = 0; (i < 32) && (terms[i] != NULL); i++) + free(terms[i]); +} + +static void cmd_connecttome(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + char *p; + struct dchub *hub; + struct socket *newsk; + struct sockaddr_in addr; + + hub = fn->data; + if((p = strchr(args, ' ')) == NULL) + return; + *(p++) = 0; + if(strcmp(args, hub->nativenick)) + return; + addr.sin_family = AF_INET; + args = p; + if((p = strchr(args, ':')) == NULL) + return; + *(p++) = 0; + addr.sin_port = htons(atoi(p)); + if(!inet_aton(args, &addr.sin_addr)) + return; + newsk = netcsconn((struct sockaddr *)&addr, sizeof(addr), (void (*)(struct socket *, int, void *))peerconnect, fn); + getfnetnode(fn); + hubhandleaction(sk, fn, cmd, args); +} + +static void sendctm(struct socket *sk, char *nick) +{ + struct sockaddr *addr; + socklen_t addrlen; + + if(tcpsock == NULL) + return; + if(sockgetremotename(tcpsock, &addr, &addrlen) < 0) + return; + if(addr->sa_family == AF_INET) + qstrf(sk, "$ConnectToMe %s %s|", nick, formataddress(addr, addrlen)); + else + flog(LOG_WARNING, "Direct Connect TCP socket is suddenly not AF_INET, but %i", addr->sa_family); + free(addr); +} + +static void cmd_revconnecttome(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + char *p; + + hub = fn->data; + if((p = strchr(args, ' ')) == NULL) + return; + *(p++) = 0; + if(strcmp(p, hub->nativenick)) + return; + sendctm(sk, args); + expectpeer(args, fn); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_getnetinfo(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + struct fnetnode *node; + int numhubs; + + hub = fn->data; + numhubs = 0; + for(node = fnetnodes; node != NULL; node = node->next) + { + if(node->state == FNN_EST) + numhubs++; + } + qstrf(sk, "$NetInfo %i$%i$%c$0|", confgetint("transfer", "slots"), numhubs, (tcpsock == NULL)?'P':'A'); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_to(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + char *p, *p2; + + hub = fn->data; + p = args; + if((p2 = strchr(p, ' ')) == NULL) + return; + *(p2++) = 0; + p = p2; + if((p2 = strchr(p, ' ')) == NULL) + return; + *(p2++) = 0; + if(strcmp(p, "From:")) + return; + p = p2; + if((p2 = strstr(p, " $")) == NULL) + return; + *p2 = 0; + p2 += 2; + hubrecvchat(fn->sk, fn, p, p2); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_sr(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + char *p, *p2; + char *nick, *filename, *hubname; + int size, slots; + struct srchres *sr; + wchar_t *wnick, *wfile; + + hub = fn->data; + nick = p = args; + if((p2 = strchr(p, ' ')) == NULL) + return; + *p2 = 0; + p = p2 + 1; + filename = p; + if((p2 = strchr(p, 5)) == NULL) + return; + *p2 = 0; + p = p2 + 1; + if((p2 = strchr(p, ' ')) == NULL) + return; + *p2 = 0; + size = atoi(p); + p = p2 + 1; + if((p2 = strchr(p, '/')) == NULL) + return; + *p2 = 0; + slots = atoi(p); + p = p2 + 1; + if((p2 = strchr(p, 5)) == NULL) + return; + p = p2 + 1; + hubname = p; + if((p2 = strstr(p, " (")) == NULL) + return; + *p2 = 0; + if((wnick = icmbstowcs(nick, DCCHARSET)) == NULL) + return; + if((wfile = icmbstowcs(filename, DCCHARSET)) == NULL) + { + free(wnick); + return; + } + sr = newsrchres(&dcnet, wfile, wnick); + if(sr->peernick != NULL) + free(sr->peernick); + sr->peernick = swcsdup(wnick); + sr->size = size; + sr->slots = slots; + free(wfile); + free(wnick); + getfnetnode(sr->fn = fn); + submitsrchres(sr); + freesrchres(sr); + hubhandleaction(sk, fn, cmd, args); +} + +static void cmd_usercommand(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + /* Do nothing for now. */ +} + +static void cmd_mynick(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + if(peer->nativename != NULL) + free(peer->nativename); + peer->nativename = sstrdup(args); + if(peer->wcsname != NULL) + free(peer->wcsname); + if((peer->wcsname = icmbstowcs(peer->nativename, DCCHARSET)) == NULL) + { + freedcpeer(peer); + return; + } + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_direction(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + char *p; + + if((p = strchr(args, ' ')) == NULL) + return; + *p = 0; + if(!strcmp(args, "Upload")) + peer->direction = TRNSD_DOWN; + if(!strcmp(args, "Download")) + peer->direction = TRNSD_UP; + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_peerlock(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + char *p; + + if((p = strchr(args, ' ')) == NULL) + return; + *p = 0; + if(!strncmp(args, "EXTENDEDPROTOCOL", 16)) + peer->extended = 1; + if(peer->key != NULL) + free(peer->key); + peer->key = dcmakekey(args); + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_key(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_filelength(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + int size; + + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + size = atoi(args); + if(peer->transfer->size == size) + { + + } else { + /* Will have to restart, then... I really hate this, but what + * am I to do then, considering the DC protocol requires the + * resume offset before it sends the filesize? */ + transfersetsize(peer->transfer, size); + freedcpeer(peer); + return; + } + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_error(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + if((peer->transfer != NULL) && (peer->transfer->dir == TRNSD_DOWN)) + { + transferseterror(peer->transfer, TRNSE_NOTFOUND); + resettransfer(peer->transfer); + return; + } + freedcpeer(peer); +} + +static void cmd_maxedout(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + if((peer->transfer != NULL) && (peer->transfer->dir == TRNSD_DOWN)) + { + transferseterror(peer->transfer, TRNSE_NOSLOTS); + resettransfer(peer->transfer); + return; + } + freedcpeer(peer); +} + +static struct +{ + char *name; + char **file; + void (*func)(void); +} lists[] = +{ + {"MyList.DcLst", &hmlistname, updatehmlist}, + {"files.xml", &xmllistname, updatexmllist}, + {"files.xml.bz2", &xmlbz2listname, updatexmlbz2list}, + {NULL, NULL} +}; + +static int openfilelist(char *name) +{ + int i, fd; + int errnobak; + + for(i = 0; lists[i].name != NULL; i++) + { + if(!strcmp(name, lists[i].name)) + break; + } + errno = 0; + if(lists[i].name == NULL) + return(-1); + fd = -1; + if((*lists[i].file == NULL) || ((fd = open(*lists[i].file, O_RDONLY)) < 0)) + lists[i].func(); + if((fd < 0) && ((*lists[i].file == NULL) || ((fd = open(*lists[i].file, O_RDONLY)) < 0))) + { + errnobak = errno; + flog(LOG_ERR, "could not open filelist tempfile: %s", strerror(errno)); + errno = errnobak; + return(-1); + } + return(fd); +} + +static struct sharecache *findbytth(char *tth32) +{ + char *buf; + size_t buflen; + struct sharecache *node; + + if((buf = base32decode(tth32, &buflen)) == NULL) + return(NULL); + if(buflen != 24) + { + free(buf); + return(NULL); + } + for(node = shareroot->child; node != NULL; node = nextscnode(node)) + { + if(node->f.b.hastth && !memcmp(node->hashtth, buf, 24)) + break; + } + free(buf); + return(node); +} + +static struct sharecache *resdcpath(char *path, char *charset, char sep) +{ + struct sharecache *node; + char *p, *p2; + + node = shareroot; + p = path; + while(p != NULL) + { + if((p2 = strchr(p, sep)) != NULL) + *(p2++) = 0; + if((node = findcache(node, icsmbstowcs(p, charset, L""))) == NULL) + return(NULL); + p = p2; + } + return(node); +} + +static void cmd_get(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + int offset; + char *p, *buf; + wchar_t *buf2; + struct sharecache *node; + struct socket *lesk; + int fd; + struct stat sb; + + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + if((p = strchr(args, '$')) == NULL) + { + freedcpeer(peer); + return; + } + *(p++) = 0; + if((offset = (atoi(p) - 1)) < 0) + { + freedcpeer(peer); + return; + } + if(((fd = openfilelist(args)) < 0) && (errno != 0)) + { + qstr(sk, "$Error Could not send file list|"); + freedcpeer(peer); + return; + } else if(fd >= 0) { + if((buf2 = icsmbstowcs(args, DCCHARSET, NULL)) != NULL) + transfersetpath(peer->transfer, buf2); + } + if(fd < 0) + { + if(slotsleft() < 1) + { + qstr(sk, "$MaxedOut|"); + freedcpeer(peer); + return; + } + if((node = resdcpath(args, DCCHARSET, '\\')) == NULL) + { + qstrf(sk, "$Error File not in share|"); + freedcpeer(peer); + return; + } + if((fd = opensharecache(node)) < 0) + { + qstrf(sk, "$Error %s|", strerror(errno)); + freedcpeer(peer); + return; + } + buf = getfspath(node); + if((buf2 = icsmbstowcs(buf, NULL, NULL)) == NULL) + flog(LOG_WARNING, "could not convert native path into a wcs (%s): %s", buf, strerror(errno)); + else + transfersetpath(peer->transfer, buf2); + free(buf); + } + if(fstat(fd, &sb) < 0) + { + close(fd); + flog(LOG_WARNING, "could not stat file %ls: %s", node->name, strerror(errno)); + qstrf(sk, "$Error|"); + freedcpeer(peer); + return; + } + if((offset != 0) && (lseek(fd, offset, SEEK_SET) < 0)) + { + close(fd); + qstrf(sk, "$Error Offset out of range|"); + freedcpeer(peer); + return; + } + lesk = wrapsock(fd); + transferprepul(peer->transfer, sb.st_size, offset, -1, lesk); + putsock(lesk); + qstrf(sk, "$FileLength %i|", peer->transfer->size); + peerhandleaction(sk, peer, cmd, args); +} + +static void starttrans(struct dcpeer *peer) +{ + peer->state = PEER_TRNS; + transferstartul(peer->transfer, peer->sk); + peer->sk->writecb = (void (*)(struct socket *, void *))transwrite; +} + +static void cmd_send(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + if(peer->transfer->localend == NULL) + { + freedcpeer(peer); + return; + } + peer->ptclose = 1; + starttrans(peer); + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_supports(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + int i; + char *p, *p2; + char **arr; + size_t arrsize, arrdata; + + if(peer->supports != NULL) + { + for(i = 0; peer->supports[i] != NULL; i++) + free(peer->supports[i]); + free(peer->supports); + } + arr = NULL; + arrsize = arrdata = 0; + p = args; + do + { + if((p2 = strchr(p, ' ')) != NULL) + *(p2++) = 0; + if(*p == 0) + continue; + addtobuf(arr, sstrdup(p)); + } while((p = p2) != NULL); + addtobuf(arr, NULL); + peer->supports = arr; + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_getblock(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + int fd; + char *p, *p2; + int start, numbytes; + char *charset, *buf; + wchar_t *buf2; + struct sharecache *node; + struct stat sb; + struct socket *lesk; + + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + p = args; + if((p2 = strchr(p, ' ')) == NULL) + { + freedcpeer(peer); + return; + } + *(p2++) = 0; + start = atoi(p); + p = p2; + if((p2 = strchr(p, ' ')) == NULL) + { + freedcpeer(peer); + return; + } + *(p2++) = 0; + numbytes = atoi(p); + p = p2; + if(!strcmp(cmd, "$UGetBlock") || !strcmp(cmd, "$UGetZBlock")) + charset = "UTF-8"; + else + charset = DCCHARSET; + if(!strcmp(cmd, "$GetZBlock") || !strcmp(cmd, "$UGetZBlock")) + initcompress(peer, CPRS_ZLIB); + if(((fd = openfilelist(p)) < 0) && (errno != 0)) + { + qstr(sk, "$Error Could not send file list|"); + return; + } else if(fd >= 0) { + if((buf2 = icsmbstowcs(args, charset, NULL)) != NULL) + transfersetpath(peer->transfer, buf2); + } + if(fd < 0) + { + if(slotsleft() < 1) + { + qstr(sk, "$MaxedOut|"); + return; + } + if((node = resdcpath(p, charset, '\\')) == NULL) + { + qstr(sk, "$Error File not in cache|"); + return; + } + if((fd = opensharecache(node)) < 0) + { + qstrf(sk, "$Error %s|", strerror(errno)); + return; + } + buf = getfspath(node); + if((buf2 = icsmbstowcs(buf, NULL, NULL)) == NULL) + flog(LOG_WARNING, "could not convert native path into a wcs (%s): %s", buf, strerror(errno)); + else + transfersetpath(peer->transfer, buf2); + free(buf); + } + if(fstat(fd, &sb) < 0) + { + close(fd); + flog(LOG_WARNING, "could not stat file %ls: %s", node->name, strerror(errno)); + qstr(sk, "$Error|"); + return; + } + if((start != 0) && ((start >= sb.st_size) || (lseek(fd, start, SEEK_SET) < 0))) + { + close(fd); + qstr(sk, "$Error Offset out of range|"); + return; + } + if((numbytes < 0) || (start + numbytes > sb.st_size)) + numbytes = sb.st_size - start; + lesk = wrapsock(fd); + transferprepul(peer->transfer, sb.st_size, start, start + numbytes, lesk); + putsock(lesk); + qstrf(sk, "$Sending %i|", numbytes); + starttrans(peer); + peerhandleaction(sk, peer, cmd, args); +} + +static void cmd_adcget(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + int i; + char **argv, *buf; + int start, numbytes; + struct sharecache *node; + struct stat sb; + struct socket *lesk; + wchar_t *wbuf; + int fd; + + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + if((argv = parseadc(args)) == NULL) + { + freedcpeer(peer); + return; + } + if(parrlen(argv) < 4) + { + freedcpeer(peer); + goto out; + } + start = atoi(argv[2]); + numbytes = atoi(argv[3]); + node = NULL; + fd = -1; + if(((fd = openfilelist(argv[1])) < 0) && (errno != 0)) + { + qstr(sk, "$Error Could not send file list|"); + goto out; + } else if(fd >= 0) { + if((wbuf = icsmbstowcs(argv[1], "UTF-8", NULL)) != NULL) + transfersetpath(peer->transfer, wbuf); + } + if(fd < 0) + { + if(slotsleft() < 1) + { + qstr(sk, "$MaxedOut|"); + goto out; + } + if(!strncmp(argv[1], "TTH/", 4)) + { + if((node = findbytth(argv[1] + 4)) == NULL) + { + qstr(sk, "$Error File not in cache|"); + goto out; + } + } else { + if((node = resdcpath(argv[1], "UTF-8", '/')) == NULL) + { + qstr(sk, "$Error File not in cache|"); + goto out; + } + } + if((fd = opensharecache(node)) < 0) + { + qstrf(sk, "$Error %s|", strerror(errno)); + goto out; + } + buf = getfspath(node); + if((wbuf = icsmbstowcs(buf, NULL, NULL)) == NULL) + flog(LOG_WARNING, "could not convert native path into a wcs (%s): %s", buf, strerror(errno)); + else + transfersetpath(peer->transfer, wbuf); + free(buf); + } + if(!strcmp(argv[0], "file")) + { + for(i = 4; argv[i] != NULL; i++) + { + if(!strcmp(argv[i], "ZL1")) + initcompress(peer, CPRS_ZLIB); + } + if(fstat(fd, &sb) < 0) + { + flog(LOG_WARNING, "could not stat file %ls: %s", node->name, strerror(errno)); + qstr(sk, "$Error|"); + goto out; + } + if((start != 0) && ((start >= sb.st_size) || (lseek(fd, start, SEEK_SET) < 0))) + { + qstr(sk, "$Error Offset out of range|"); + goto out; + } + if((numbytes < 0) || (start + numbytes > sb.st_size)) + numbytes = sb.st_size - start; + lesk = wrapsock(fd); + transferprepul(peer->transfer, sb.st_size, start, start + numbytes, lesk); + putsock(lesk); + fd = -1; + qstr(sk, "$ADCSND"); + sendadc(sk, "file"); + sendadc(sk, argv[1]); + sendadcf(sk, "%i", start); + sendadcf(sk, "%i", numbytes); + if(peer->compress == CPRS_ZLIB) + sendadc(sk, "ZL1"); + qstr(sk, "|"); + starttrans(peer); + } else if(!strcmp(argv[0], "tthl")) { + /* + * XXX: Implement full TTHL support. + * + * In the meantime, this is better than nothing, since it at + * least allows fetching of the TTH root if it isn't already + * known. + */ + if(node == NULL) + { + qstr(sk, "$Error no TTHL data for virtual files|"); + goto out; + } + qstr(sk, "$ADCSND"); + sendadc(sk, "tthl"); + sendadc(sk, argv[1]); + sendadc(sk, "0"); + sendadc(sk, "24"); + qstr(sk, "|"); + sockqueue(sk, node->hashtth, 24); + } else { + qstr(sk, "$Error Namespace not implemented|"); + goto out; + } + peerhandleaction(sk, peer, cmd, args); + + out: + if(fd >= 0) + close(fd); + freeparr(argv); +} + +/* +Hub skeleton: +static void cmd_(struct socket *sk, struct fnetnode *fn, char *cmd, char *args) +{ + struct dchub *hub; + + hub = fn->data; + hubhandleaction(sk, fn, cmd, args); +} + +Peer skeleton: +static void cmd_(struct socket *sk, struct dcpeer *peer, char *cmd, char *args) +{ + peerhandleaction(sk, peer, cmd, args); +} + +*/ + +static int hubreqconn(struct fnetpeer *peer) +{ + struct dchub *hub; + char *mbsnick; + + if((peer->fn->state != FNN_EST) || (peer->fn->fnet != &dcnet)) + { + errno = EINVAL; + return(1); + } + if((hub = peer->fn->data) == NULL) + { + errno = EFAULT; + return(1); + } + if((mbsnick = icwcstombs(peer->id, DCCHARSET)) == NULL) + return(1); /* Shouldn't happen, of course, but who knows... */ + if(tcpsock != NULL) + { + sendctm(peer->fn->sk, mbsnick); + expectpeer(mbsnick, peer->fn); + } else { + qstrf(peer->fn->sk, "$RevConnectToMe %s %s|", hub->nativenick, mbsnick); + } + free(mbsnick); + return(0); +} + +static int hubsendchat(struct fnetnode *fn, int public, wchar_t *to, wchar_t *string) +{ + struct dchub *hub; + char *mbsstring, *mbsto; + + hub = fn->data; + if((mbsto = icwcstombs(to, DCCHARSET)) == NULL) + { + errno = EILSEQ; + return(1); + } + if((mbsstring = icwcstombs(string, DCCHARSET)) == NULL) + { + errno = EILSEQ; + return(1); + } + if((strchr(mbsto, '|') != NULL) || (strchr(mbsto, ' ') != NULL)) + { + free(mbsto); + free(mbsstring); + errno = ESRCH; + return(1); + } + if(strchr(mbsstring, '|') != NULL) + { + free(mbsto); + free(mbsstring); + errno = EILSEQ; + return(1); + } + if(public) + { + if(*to == L'\0') + { + qstrf(fn->sk, "<%s> %s|", hub->nativenick, mbsstring); + } else { + qstrf(fn->sk, "$To: %s From: %s $<%s> %s|", mbsto, hub->nativenick, hub->nativenick, mbsstring); + } + } else { + qstrf(fn->sk, "$To: %s From: %s $<%s> %s|", mbsto, hub->nativenick, hub->nativenick, mbsstring); + } + free(mbsto); + free(mbsstring); + return(0); +} + +static void findsizelimit(struct sexpr *sexpr, int *min, int *max) +{ + int minl, maxl, minr, maxr, retmin, retmax; + + switch(sexpr->op) + { + case SOP_AND: + findsizelimit(sexpr->l, &minl, &maxl); + findsizelimit(sexpr->r, &minr, &maxr); + retmin = (minl > minr)?minl:minr; + if((maxl != -1) && (maxr != -1)) + retmax = (maxl < maxr)?maxl:maxr; + else if(maxl != -1) + retmax = maxl; + else if(maxr != -1) + retmax = maxr; + else + retmax = -1; + break; + case SOP_OR: + findsizelimit(sexpr->l, &minl, &maxl); + findsizelimit(sexpr->r, &minr, &maxr); + retmin = (minl < minr)?minl:minr; + if((maxl == -1) || (maxr == -1)) + retmax = -1; + else + retmax = (maxl > maxr)?maxl:maxr; + break; + case SOP_NOT: + findsizelimit(sexpr->l, &minl, &maxl); + if((minl == 0) && (maxl == -1)) /* Interval is unspecified */ + { + retmin = 0; + retmax = -1; + } else if((minl == 0) && (maxl != -1)) { + retmin = maxl + 1; + retmax = -1; + } else if((minl != 0) && (maxl == -1)) { + retmin = 0; + retmax = minl - 1; + } else { /* This would yield two seperate intervals, which DC cannot handle */ + retmin = 0; + retmax = -1; + } + case SOP_SIZELT: + retmin = 0; + retmax = sexpr->d.n - 1; + break; + case SOP_SIZEEQ: + retmin = sexpr->d.n; + retmax = sexpr->d.n; + break; + case SOP_SIZEGT: + retmin = sexpr->d.n + 1; + retmax = -1; + break; + default: + retmin = 0; + retmax = -1; + break; + } + if(min != NULL) + *min = retmin; + if(max != NULL) + *max = retmax; +} + +static int hubsearch(struct fnetnode *fn, struct search *srch, struct srchfnnlist *ln) +{ + struct dchub *hub; + struct wcslist *list, *cur; + char *sstr, *buf, *p; + size_t sstrsize, sstrdata; + struct sockaddr *name; + socklen_t namelen; + int minsize, maxsize; + char sizedesc[32]; + + hub = fn->data; + if((fn->state != FNN_EST) || (fn->sk == NULL) || (fn->sk->state != SOCK_EST)) + return(1); + list = findsexprstrs(srch->sexpr); + findsizelimit(srch->sexpr, &minsize, &maxsize); + if((minsize != 0) && (maxsize != -1)) + { + /* Choose either minsize or maxsize by trying to determine + * which choice will be most restrictive. The result can be + * approximative at best anyway, so don't try too hard... */ + if((50000000 - maxsize) > (minsize - 50000000)) + minsize = 0; + else + maxsize = -1; + } + if(minsize != 0) + { + snprintf(sizedesc, sizeof(sizedesc), "T?F?%i", minsize); + } else if(maxsize != -1) { + snprintf(sizedesc, sizeof(sizedesc), "T?T?%i", maxsize); + } else { + strcpy(sizedesc, "F?F?0"); + } + sstr = NULL; + sstrsize = sstrdata = 0; + if(list != NULL) + { + for(cur = list; cur != NULL; cur = cur->next) + { + if((buf = icwcstombs(cur->str, DCCHARSET)) == NULL) + { + /* Can't find anything anyway if the search expression + * requires characters outside DC's charset. There's + * nothing technically wrong with the search itself, + * however, so return success. This should be + * considered as an optimization. */ + freesl(&list); + if(sstr != NULL) + free(sstr); + return(0); + } + if(cur != list) + addtobuf(sstr, '$'); + /* + * It probably doesn't hurt if buf contains any extra + * dollar signs - it will just result in extra search + * terms, and the extraneous results will be filtered by + * the search layer anyway. It hurts if it contains any + * pipes, though, so let's sell them for money. + */ + for(p = buf; *p; p++) + { + if(*p == '|') + *p = '$'; + } + bufcat(sstr, buf, strlen(buf)); + free(buf); + } + } else { + /* Will match all files... :-/ */ + addtobuf(sstr, '.'); + } + addtobuf(sstr, 0); + if(tcpsock != NULL) + { + if(sockgetremotename(udpsock, &name, &namelen) < 0) + { + flog(LOG_WARNING, "cannot get address of UDP socket"); + } else { + qstrf(fn->sk, "$Search %s %s?1?%s|", formataddress(name, namelen), sizedesc, sstr); + free(name); + } + } else { + qstrf(fn->sk, "$Search Hub:%s %s?1?%s|", hub->nativenick, sizedesc, sstr); + } + free(sstr); + freesl(&list); + fn->lastsrch = time(NULL); + return(0); +} + +#undef skipcmd +#undef qstr +#undef qstrf + +#define cc(c) ((void (*)(struct socket *, void *, char *, char *))(c)) +struct command hubcmds[] = +{ + {"$Lock", cc(cmd_lock)}, + {"$HubName", cc(cmd_hubname)}, + {"$Hello", cc(cmd_hello)}, + {"$Quit", cc(cmd_quit)}, + {"$NickList", cc(cmd_nicklist)}, + {"$OpList", cc(cmd_oplist)}, + {"$MyINFO", cc(cmd_myinfo)}, + {"$ForceMove", cc(cmd_forcemove)}, + {"$Search", cc(cmd_search)}, + {"$MultiSearch", cc(cmd_search)}, + {"$ConnectToMe", cc(cmd_connecttome)}, + {"$RevConnectToMe", cc(cmd_revconnecttome)}, + {"$GetNetInfo", cc(cmd_getnetinfo)}, + {"$To:", cc(cmd_to)}, + {"$SR", cc(cmd_sr)}, + {"$UserCommand", cc(cmd_usercommand)}, + {NULL, NULL} +}; + +struct command peercmds[] = +{ + {"$MyNick", cc(cmd_mynick)}, + {"$Lock", cc(cmd_peerlock)}, + {"$Direction", cc(cmd_direction)}, + {"$Key", cc(cmd_key)}, + {"$FileLength", cc(cmd_filelength)}, + {"$Error", cc(cmd_error)}, + {"$MaxedOut", cc(cmd_maxedout)}, + {"$Get", cc(cmd_get)}, + {"$Send", cc(cmd_send)}, + {"$Supports", cc(cmd_supports)}, + {"$GetBlock", cc(cmd_getblock)}, + {"$UGetBlock", cc(cmd_getblock)}, + {"$GetZBlock", cc(cmd_getblock)}, + {"$UGetZBlock", cc(cmd_getblock)}, + {"$ADCGET", cc(cmd_adcget)}, + {NULL, NULL} +}; +#undef cc + +static void dctransdetach(struct transfer *transfer, struct dcpeer *peer) +{ + CBUNREG(transfer, trans_filterout, peer); + if(peer->freeing) + return; + peer->transfer = NULL; + freedcpeer(peer); +} + +static void dctransgotdata(struct transfer *transfer, struct dcpeer *peer) +{ + int ret; + void *buf; + char outbuf[1024]; + z_stream *cstr; + size_t bufsize; + + if((peer->state == PEER_TRNS) || (peer->state == PEER_SYNC)) + { + if(sockqueuesize(peer->sk) < 65536) + { + if((buf = transfergetdata(transfer, &bufsize)) != NULL) + { + if(peer->compress == CPRS_NONE) + { + sockqueue(peer->sk, buf, bufsize); + } else if(peer->compress == CPRS_ZLIB) { + cstr = peer->cprsdata; + cstr->next_in = buf; + cstr->avail_in = bufsize; + while(cstr->avail_in > 0) + { + cstr->next_out = outbuf; + cstr->avail_out = sizeof(outbuf); + if((ret = deflate(cstr, 0)) != Z_OK) + { + flog(LOG_WARNING, "bug? deflate() did not return Z_OK (but rather %i)", ret); + freedcpeer(peer); + return; + } + sockqueue(peer->sk, outbuf, sizeof(outbuf) - cstr->avail_out); + } + } + free(buf); + } + if(peer->state == PEER_SYNC) + { + if(peer->compress == CPRS_ZLIB) + { + cstr = peer->cprsdata; + cstr->next_in = NULL; + cstr->avail_in = 0; + do + { + cstr->next_out = outbuf; + cstr->avail_out = sizeof(outbuf); + ret = deflate(cstr, Z_FINISH); + if((ret != Z_OK) && (ret != Z_STREAM_END)) + { + flog(LOG_WARNING, "bug? deflate(Z_FINISH) did not return Z_OK (but rather %i)", ret); + freedcpeer(peer); + return; + } + sockqueue(peer->sk, outbuf, sizeof(outbuf) - cstr->avail_out); + } while(ret != Z_STREAM_END); + } + if(peer->ptclose) + { + freedcpeer(peer); + } else { + peer->state = PEER_CMD; + endcompress(peer); + transfersetstate(transfer, TRNS_HS); + socksettos(peer->sk, confgetint("fnet", "fnptos")); + peer->sk->writecb = NULL; + } + } + } + } +} + +static void dctransendofdata(struct transfer *transfer, struct dcpeer *peer) +{ + peer->state = PEER_SYNC; + dctransgotdata(transfer, peer); +} + +static void dcwantdata(struct transfer *transfer, struct dcpeer *peer) +{ + if(transferdatasize(transfer) < 65536) + peer->sk->ignread = 0; +} + +static void transread(struct socket *sk, struct dcpeer *peer) +{ + void *buf; + size_t bufsize; + struct transfer *transfer; + + if((buf = sockgetinbuf(sk, &bufsize)) == NULL) + return; + if(peer->transfer == NULL) + { + free(buf); + freedcpeer(peer); + return; + } + transferputdata(peer->transfer, buf, bufsize); + free(buf); + if(peer->transfer->curpos >= peer->transfer->size) + { + transfer = peer->transfer; + transferdetach(transfer); + transferendofdata(transfer); + return; + } + if(transferdatasize(peer->transfer) > 65535) + sk->ignread = 1; +} + +static void transerr(struct socket *sk, int err, struct dcpeer *peer) +{ + struct transfer *transfer; + + if((transfer = peer->transfer) == NULL) + { + freedcpeer(peer); + return; + } + transferdetach(transfer); + transferendofdata(transfer); +} + +static void transwrite(struct socket *sk, struct dcpeer *peer) +{ + if((peer->state != PEER_TRNS) && (peer->state != PEER_SYNC)) + return; + if(peer->transfer == NULL) + { + freedcpeer(peer); + return; + } + dctransgotdata(peer->transfer, peer); +} + +static void udpread(struct socket *sk, void *data) +{ + char *buf, *p, *p2; + size_t buflen; + char *nick, *filename, *hubname; + int size, slots; + struct fnetnode *fn, *myfn; + struct dchub *hub; + struct srchres *sr; + wchar_t *wnick, *wfile; + + if((buf = sockgetinbuf(sk, &buflen)) == NULL) + return; + buf = srealloc(buf, buflen + 1); + buf[buflen] = 0; + if(!strncmp(buf, "$SR ", 4)) + { + p = buf + 4; + nick = p; + if((p2 = strchr(p, ' ')) == NULL) + { + free(buf); + return; + } + *p2 = 0; + p = p2 + 1; + filename = p; + if((p2 = strchr(p, 5)) == NULL) + { + free(buf); + return; + } + *p2 = 0; + p = p2 + 1; + if((p2 = strchr(p, ' ')) == NULL) + { + free(buf); + return; + } + *p2 = 0; + size = atoi(p); + p = p2 + 1; + if((p2 = strchr(p, '/')) == NULL) + { + free(buf); + return; + } + *p2 = 0; + slots = atoi(p); + p = p2 + 1; + if((p2 = strchr(p, 5)) == NULL) + { + free(buf); + return; + } + p = p2 + 1; + hubname = p; + if((p2 = strstr(p, " (")) == NULL) + { + free(buf); + return; + } + *p2 = 0; + if((wnick = icmbstowcs(nick, DCCHARSET)) == NULL) + { + free(buf); + return; + } + if((wfile = icmbstowcs(filename, DCCHARSET)) == NULL) + { + free(wnick); + free(buf); + return; + } + myfn = NULL; + for(fn = fnetnodes; fn != NULL; fn = fn->next) + { + if((fn->fnet == &dcnet) && ((hub = fn->data) != NULL)) + { + if((hub->nativename != NULL) && !strcmp(hub->nativename, hubname)) + { + if(myfn == NULL) + { + myfn = fn; + } else { + myfn = NULL; + break; + } + } + } + } + sr = newsrchres(&dcnet, wfile, wnick); + if(sr->peernick != NULL) + free(sr->peernick); + sr->peernick = swcsdup(wnick); + sr->size = size; + sr->slots = slots; + free(wfile); + free(wnick); + if(myfn != NULL) + getfnetnode(sr->fn = myfn); + submitsrchres(sr); + freesrchres(sr); + } + free(buf); +} + +static void hubread(struct socket *sk, struct fnetnode *fn) +{ + struct dchub *hub; + char *newbuf; + size_t datalen; + char *p; + + hub = (struct dchub *)fn->data; + if((newbuf = sockgetinbuf(sk, &datalen)) == NULL) + return; + if(hub->inbufdata > 500000) /* Discard possible malicious data */ + hub->inbufdata = 0; + sizebuf2(hub->inbuf, hub->inbufdata + datalen, 1); + memcpy(hub->inbuf + hub->inbufdata, newbuf, datalen); + free(newbuf); + p = hub->inbuf + hub->inbufdata; + hub->inbufdata += datalen; + while((datalen > 0) && ((p = memchr(p, '|', datalen)) != NULL)) + { + *(p++) = 0; + newqcmd(&hub->queue, hub->inbuf); + memmove(hub->inbuf, p, hub->inbufdata -= p - hub->inbuf); + datalen = hub->inbufdata; + p = hub->inbuf; + } +} + +static void huberr(struct socket *sk, int err, struct fnetnode *fn) +{ + killfnetnode(fn); +} + +static int hubsetnick(struct fnetnode *fn, wchar_t *newnick) +{ + struct dchub *hub; + char *buf; + + hub = fn->data; + if((buf = icwcstombs(newnick, DCCHARSET)) == NULL) + return(1); + if((strchr(buf, ' ') != NULL) || (strchr(buf, '|') != NULL) || (strchr(buf, '$') != NULL)) + { + free(buf); + return(1); + } + if(hub == NULL) /* Not yet connected */ + { + free(buf); + return(0); + } + if(hub->nativenick != NULL) + free(hub->nativenick); + hub->nativenick = buf; + return(0); +} + +static struct dchub *newdchub(struct fnetnode *fn) +{ + struct dchub *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + fn->data = new; + if(hubsetnick(fn, fn->mynick)) + fnetsetnick(fn, L"DoldaConnectUser-IN"); + /* IN as in Invalid Nick */ + return(new); +} + +static struct dcpeer *newdcpeer(struct socket *sk) +{ + struct dcpeer *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + new->transfer = NULL; + getsock(sk); + new->sk = sk; + new->next = peers; + new->prev = NULL; + if(peers != NULL) + peers->prev = new; + peers = new; + numdcpeers++; + return(new); +} + +static void freedcpeer(struct dcpeer *peer) +{ + int i; + struct qcommand *qcmd; + + peer->freeing = 1; + if(peers == peer) + peers = peer->next; + if(peer->next != NULL) + peer->next->prev = peer->prev; + if(peer->prev != NULL) + peer->prev->next = peer->next; + if(peer->transfer != NULL) + { + if(peer->transfer->dir == TRNSD_UP) + peer->transfer->close = 1; + if(peer->transfer->dir == TRNSD_DOWN) + resettransfer(peer->transfer); + transferdetach(peer->transfer); + } + if(peer->sk->data == peer) + peer->sk->data = NULL; + peer->sk->readcb = NULL; + peer->sk->writecb = NULL; + peer->sk->errcb = NULL; + putsock(peer->sk); + endcompress(peer); + if(peer->supports != NULL) + { + for(i = 0; peer->supports[i] != NULL; i++) + free(peer->supports[i]); + free(peer->supports); + } + if(peer->mbspath != NULL) + free(peer->mbspath); + if(peer->inbuf != NULL) + free(peer->inbuf); + if(peer->key != NULL) + free(peer->key); + if(peer->wcsname != NULL) + free(peer->wcsname); + if(peer->nativename != NULL) + free(peer->nativename); + if(peer->fn != NULL) + putfnetnode(peer->fn); + while((qcmd = ulqcmd(&peer->queue)) != NULL) + freeqcmd(qcmd); + free(peer); + numdcpeers--; +} + +static void hubconnect(struct fnetnode *fn) +{ + fn->sk->readcb = (void (*)(struct socket *, void *))hubread; + fn->sk->errcb = (void (*)(struct socket *, int, void *))huberr; + getfnetnode(fn); + fn->data = newdchub(fn); + fn->sk->data = fn; + return; +} + +static void hubdestroy(struct fnetnode *fn) +{ + struct dchub *hub; + struct qcommand *qcmd; + + hub = (struct dchub *)fn->data; + if(fn->sk != NULL) + { + if(fn->sk->data == fn) + { + fn->sk->data = NULL; + putfnetnode(fn); + } + } + if(hub == NULL) + return; + while((qcmd = ulqcmd(&hub->queue)) != NULL) + freeqcmd(qcmd); + if(hub->nativename != NULL) + free(hub->nativename); + if(hub->nativenick != NULL) + free(hub->nativenick); + if(hub->inbuf != NULL) + free(hub->inbuf); + free(hub); +} + +static wchar_t *dcbasename(wchar_t *filename) +{ + wchar_t *ret; + + if((ret = wcsrchr(filename, L'\\')) != NULL) + return(ret + 1); + return(filename); +} + +static struct transferiface dctransfer = +{ + .detach = (void (*)(struct transfer *, void *))dctransdetach, + .gotdata = (void (*)(struct transfer *, void *))dctransgotdata, + .endofdata = (void (*)(struct transfer *, void *))dctransendofdata, + .wantdata = (void (*)(struct transfer *, void *))dcwantdata +}; + +static struct fnet dcnet = +{ + .name = L"dc", + .connect = hubconnect, + .destroy = hubdestroy, + .setnick = hubsetnick, + .reqconn = hubreqconn, + .sendchat = hubsendchat, + .search = hubsearch, + .filebasename = dcbasename +}; + +static void peerread(struct socket *sk, struct dcpeer *peer) +{ + char *newbuf, *p; + size_t datalen; + + if((newbuf = sockgetinbuf(sk, &datalen)) == NULL) + return; + sizebuf2(peer->inbuf, peer->inbufdata + datalen, 1); + memcpy(peer->inbuf + peer->inbufdata, newbuf, datalen); + free(newbuf); + p = peer->inbuf + peer->inbufdata; + peer->inbufdata += datalen; + while((datalen > 0) && (p = memchr(p, '|', datalen)) != NULL) + { + *(p++) = 0; + newqcmd(&peer->queue, peer->inbuf); + memmove(peer->inbuf, p, peer->inbufdata -= p - peer->inbuf); + datalen = peer->inbufdata; + p = peer->inbuf; + } +} + +static void peererror(struct socket *sk, int err, struct dcpeer *peer) +{ + freedcpeer(peer); +} + +static void peerconnect(struct socket *sk, int err, struct fnetnode *fn) +{ + struct dcpeer *peer; + + if(err != 0) + { + putfnetnode(fn); + return; + } + peer = newdcpeer(sk); + peer->fn = fn; + peer->accepted = 0; + sk->readcb = (void (*)(struct socket *, void *))peerread; + sk->errcb = (void (*)(struct socket *, int, void *))peererror; + sk->data = peer; + socksettos(sk, confgetint("fnet", "fnptos")); + peerhandleaction(sk, peer, NULL, NULL); + putsock(sk); +} + +static void peeraccept(struct socket *sk, struct socket *newsk, void *data) +{ + struct dcpeer *peer; + + peer = newdcpeer(newsk); + peer->accepted = 1; + newsk->readcb = (void (*)(struct socket *, void *))peerread; + newsk->errcb = (void (*)(struct socket *, int, void *))peererror; + newsk->data = peer; + socksettos(newsk, confgetint("fnet", "fnptos")); + peerhandleaction(sk, peer, NULL, NULL); +} + +static void updatehmlist(void) +{ + int i, lev, ic, ret; + struct sharecache *node; + char *buf, *buf2, numbuf[32]; + size_t bufsize, bufdata; + int fd, ibuf; + + bufdata = 0; + buf = smalloc(bufsize = 65536); + node = shareroot->child; + lev = 0; + while(1) + { + ic = 0; + if((buf2 = icwcstombs(node->name, DCCHARSET)) != NULL) + { + for(i = 0; i < lev; i++) + addtobuf(buf, 9); + bufcat(buf, buf2, strlen(buf2)); + free(buf2); + if(node->f.b.type == FILE_REG) + { + addtobuf(buf, '|'); + sprintf(numbuf, "%i", node->size); + bufcat(buf, numbuf, strlen(numbuf)); + } + addtobuf(buf, 13); + addtobuf(buf, 10); + } else { + ic = 1; + } + if((node->child != NULL) && !ic) + { + lev++; + node = node->child; + } else if(node->next != NULL) { + node = node->next; + } else { + while(node->next == NULL) + { + lev--; + node = node->parent; + if(node == shareroot) + break; + } + if(node == shareroot) + break; + node = node->next; + } + } + if(hmlistname != NULL) + { + unlink(hmlistname); + free(hmlistname); + } + hmlistname = sstrdup("/tmp/dc-filelist-hm-XXXXXX"); + if((fd = mkstemp(hmlistname)) < 0) + { + flog(LOG_WARNING, "could not create HM file list tempfile: %s", strerror(errno)); + free(hmlistname); + hmlistname = NULL; + } else { + /* + * I do not want to implement a good Huffman encoder, and it's not + * like Huffman encoding actually yields any impressive results + * for DC file lists anyway, so I'll just output a bogus + * tree. Implement a good encoder if you want to. + */ + write(fd, "HE3\r\0", 5); + write(fd, &bufdata, 4); + ibuf = 256; + write(fd, &ibuf, 2); + ibuf = 8; + for(i = 0; i < 256; i++) + { + write(fd, &i, 1); + write(fd, &ibuf, 1); + } + for(i = 0; i < 256; i++) + write(fd, &i, 1); + for(buf2 = buf; bufdata > 0;) + { + if((ret = write(fd, buf2, bufdata)) < 0) + { + flog(LOG_WARNING, "could not write file list: %s", strerror(errno)); + break; + } + bufdata -= ret; + buf2 += ret; + } + close(fd); + } + free(buf); +} + +static struct xmlent +{ + wchar_t c; + wchar_t *ent; +} entities[] = { + {L'&', L"&"}, + {L'\"', L"""}, +/* {L'\'', L"""}, Emulate DC++ escaping by omitting this. */ + {L'\0', NULL} +}; + +static wchar_t *escapexml(wchar_t *src) +{ + static wchar_t *buf = NULL;; + int ret, c; + wchar_t nbuf[32]; + size_t bufsize, bufdata; + struct xmlent *ent; + + if(buf != NULL) + free(buf); + buf = NULL; + bufsize = bufdata = 0; + for(; *src != L'\0'; src++) + { + c = wctob(*src); + if((c > 0) && (c < 32)) + { + bufcat(buf, L"&#", 2); + ret = swprintf(nbuf, (sizeof(nbuf) / sizeof(*nbuf)), L"%i", c); + bufcat(buf, nbuf, ret); + addtobuf(buf, L';'); + } else { + for(ent = entities; ent->ent != NULL; ent++) + { + if(ent->c == *src) + { + bufcat(buf, ent->ent, wcslen(ent->ent)); + break; + } + } + if(ent->ent == NULL) + addtobuf(buf, *src); + } + } + addtobuf(buf, L'\0'); + return(buf); +} + +static void updatexmllist(void) +{ + int i, fd, lev; + FILE *fs; + char cidbuf[14], *namebuf; + char *hashbuf; + struct sharecache *node; + + if(xmllistname != NULL) + { + unlink(xmllistname); + free(xmllistname); + } + xmllistname = sstrdup("/tmp/dc-filelist-dcxml-XXXXXX"); + if((fd = mkstemp(xmllistname)) < 0) + { + flog(LOG_WARNING, "could not create XML file list tempfile: %s", strerror(errno)); + free(xmllistname); + xmllistname = NULL; + return; + } + if((fs = fdopen(fd, "w")) == NULL) + { + flog(LOG_WARNING, "could not fdopen XML list fd %i: %s", fd, strerror(errno)); + unlink(xmllistname); + free(xmllistname); + xmllistname = NULL; + close(fd); + return; + } + fprintf(fs, "\r\n"); + for(i = 0; i < sizeof(cidbuf) - 1; i++) + cidbuf[i] = (rand() % ('Z' - 'A' + 1)) + 'A'; + cidbuf[i] = 0; + fprintf(fs, "\r\n", cidbuf, DCIDFULL); + + node = shareroot->child; + lev = 0; + while(1) + { + if((namebuf = icswcstombs(escapexml(node->name), "UTF-8", NULL)) != NULL) + { + for(i = 0; i < lev; i++) + fputc('\t', fs); + if(node->child != NULL) + { + fprintf(fs, "\r\n", namebuf); + node = node->child; + lev++; + continue; + } else { + fprintf(fs, "size); + if(node->f.b.hastth) + { + hashbuf = base32encode(node->hashtth, 24); + fprintf(fs, " TTH=\"%.39s\"", hashbuf); + free(hashbuf); + } + fprintf(fs, "/>\r\n"); + } + while(node->next == NULL) + { + node = node->parent; + if(node == shareroot) + { + break; + } else { + lev--; + for(i = 0; i < lev; i++) + fputc('\t', fs); + fprintf(fs, "\r\n"); + } + } + if(node == shareroot) + break; + node = node->next; + } + } + +#ifdef DCPP_MASQUERADE + fprintf(fs, ""); +#else + fprintf(fs, "\r\n"); +#endif + fclose(fs); +} + +static void updatexmlbz2list(void) +{ + int err, fd; + FILE *in; + FILE *real; + BZFILE *out; + char buf[1024]; + size_t bufdata; + + if(xmllistname == NULL) + return; + if((in = fopen(xmllistname, "r")) == NULL) + { + flog(LOG_WARNING, "could not open XML file list for bzipping: %s", strerror(errno)); + return; + } + if(xmlbz2listname != NULL) + { + unlink(xmlbz2listname); + free(xmlbz2listname); + } + xmlbz2listname = sstrdup("/tmp/dc-filelist-dcxmlbz2-XXXXXX"); + if((fd = mkstemp(xmlbz2listname)) < 0) + { + flog(LOG_WARNING, "could not create bzipped XML file list tempfile: %s", strerror(errno)); + free(xmlbz2listname); + xmlbz2listname = NULL; + fclose(in); + return; + } + if((real = fdopen(fd, "w")) == NULL) + { + flog(LOG_WARNING, "could not fdopen bzipped XML list fd %i: %s", fd, strerror(errno)); + close(fd); + unlink(xmlbz2listname); + free(xmlbz2listname); + xmlbz2listname = NULL; + fclose(in); + return; + } + out = BZ2_bzWriteOpen(&err, real, 9, 0, 0); + if(err != BZ_OK) + { + flog(LOG_WARNING, "could not open bzip2 stream from XML list"); + fclose(real); + unlink(xmlbz2listname); + free(xmlbz2listname); + xmlbz2listname = NULL; + fclose(in); + return; + } + while(!feof(in)) + { + bufdata = fread(buf, 1, sizeof(buf), in); + BZ2_bzWrite(&err, out, buf, bufdata); + } + fclose(in); + BZ2_bzWriteClose(&err, out, 0, NULL, NULL); + fclose(real); +} + +static int shareupdate(unsigned long long uusharesize, void *data) +{ + updatehmlist(); + updatexmllist(); + updatexmlbz2list(); + return(0); +} + +static void dispatchcommand(struct qcommand *qcmd, struct command *cmdlist, struct socket *sk, void *data) +{ + char *p; + struct command *cmd; + + if((p = strchr(qcmd->string, ' ')) != NULL) + *(p++) = 0; + for(cmd = cmdlist; cmd->handler != NULL; cmd++) + { + if(!strcmp(cmd->name, qcmd->string)) + break; + } + if(cmd->handler != NULL) + cmd->handler(sk, data, qcmd->string, p); +/* + else + flog(LOG_DEBUG, "Unimplemented DC command: %s \"%s\"", qcmd->string, p?p:"noargs"); +*/ +} + +static int run(void) +{ + struct fnetnode *fn, *nextfn; + struct dchub *hub; + struct dcpeer *peer, *nextpeer; + struct qcommand *qcmd; + int ret; + + ret = 0; + for(fn = fnetnodes; fn != NULL; fn = nextfn) + { + nextfn = fn->next; + if(fn->fnet != &dcnet) + continue; + if(fn->data == NULL) + continue; + hub = (struct dchub *)fn->data; + if((qcmd = ulqcmd(&hub->queue)) != NULL) + { + if(*qcmd->string == '$') + { + if((fn->sk != NULL) && (fn->sk->state == SOCK_EST)) + dispatchcommand(qcmd, hubcmds, fn->sk, fn); + } else if(*qcmd->string != 0) { + hubrecvchat(fn->sk, fn, NULL, qcmd->string); + } + freeqcmd(qcmd); + ret = 1; + break; + } + } + for(peer = peers; peer != NULL; peer = nextpeer) + { + nextpeer = peer->next; + if((qcmd = ulqcmd(&peer->queue)) != NULL) + { + if(*qcmd->string == '$') + dispatchcommand(qcmd, peercmds, peer->sk, peer); + freeqcmd(qcmd); + ret = 1; + break; + } + } + return(ret); +} + +static void preinit(int hup) +{ + if(hup) + return; + regfnet(&dcnet); +} + +static int updateudpport(struct configvar *var, void *uudata) +{ + struct sockaddr_in addr; + struct socket *newsock; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(var->val.num); + if((newsock = netcsdgram((struct sockaddr *)&addr, sizeof(addr))) == NULL) + { + flog(LOG_WARNING, "could not create new DC UDP socket, reverting to old: %s", strerror(errno)); + return(0); + } + newsock->readcb = udpread; + if(udpsock != NULL) + putsock(udpsock); + udpsock = newsock; + return(0); +} + +static int updatetcpport(struct configvar *var, void *uudata) +{ + struct sockaddr_in addr; + struct socket *newsock; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(var->val.num); + if((newsock = netcslisten(SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr), peeraccept, NULL)) == NULL) + flog(LOG_INFO, "could not listen to a remote address, going into passive mode"); + if(tcpsock != NULL) + putsock(tcpsock); + tcpsock = newsock; + return(0); +} + +static int init(int hup) +{ + struct sockaddr_in addr; + + if(!hup) + { + GCBREG(sharechangecb, shareupdate, NULL); + if(udpsock != NULL) + putsock(udpsock); + if(tcpsock != NULL) + putsock(tcpsock); + addr.sin_family = AF_INET; + memset(&addr.sin_addr, 0, sizeof(addr.sin_addr)); + addr.sin_port = htons(confgetint("dc", "udpport")); + if((udpsock = netcsdgram((struct sockaddr *)&addr, sizeof(addr))) == NULL) + { + flog(LOG_CRIT, "could not create DC UDP socket: %s", strerror(errno)); + return(1); + } + udpsock->readcb = udpread; + addr.sin_port = htons(confgetint("dc", "tcpport")); + if((tcpsock = netcslisten(SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr), peeraccept, NULL)) == NULL) + flog(LOG_INFO, "could not listen to a remote address, going into passive mode"); + CBREG(confgetvar("dc", "udpport"), conf_update, updateudpport, NULL, NULL); + CBREG(confgetvar("dc", "tcpport"), conf_update, updatetcpport, NULL, NULL); + CBREG(confgetvar("net", "mode"), conf_update, updatetcpport, NULL, NULL); + } + return(0); +} + +static void terminate(void) +{ + if(hmlistname != NULL) + { + unlink(hmlistname); + free(hmlistname); + } + if(xmllistname != NULL) + { + unlink(xmllistname); + free(xmllistname); + } + if(xmlbz2listname != NULL) + { + unlink(xmlbz2listname); + free(xmlbz2listname); + } +} + +static struct configvar myvars[] = +{ + {CONF_VAR_STRING, "desc", {.str = L""}}, + {CONF_VAR_STRING, "speedstring", {.str = L"LAN(T1)"}}, + {CONF_VAR_STRING, "email", {.str = L"spam@spam.org"}}, + {CONF_VAR_INT, "udpport", {.num = 0}}, + {CONF_VAR_INT, "tcpport", {.num = 0}}, + {CONF_VAR_END} +}; + +static struct module me = +{ + .conf = + { + .vars = myvars + }, + .preinit = preinit, + .init = init, + .run = run, + .terminate = terminate, + .name = "dc" +}; + +MODULE(me) diff --git a/daemon/log.c b/daemon/log.c new file mode 100644 index 0000000..49a444c --- /dev/null +++ b/daemon/log.c @@ -0,0 +1,75 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "utils.h" + +int logtostderr, logtosyslog; +int syslogfac = LOG_DAEMON; +static int inited = 0; + +void flog(int priority, char *format, ...) +{ + va_list args; + char *b; + struct iovec iov[2]; + + va_start(args, format); + if((b = vsprintf2(format, args)) == NULL) + { + if(logtostderr) + fputs("No memory available for logging\n", stderr); + if(logtosyslog) + syslog(LOG_CRIT, "No memory available for logging"); + } + va_end(args); + if(logtostderr) + { + iov[0].iov_base = b; + iov[0].iov_len = strlen(b); + iov[1].iov_base = "\n"; + iov[1].iov_len = 1; + writev(2, iov, 2); + } + if(logtosyslog) + syslog(priority, "%s", b); + free(b); +} + +void initlog(void) +{ + if(inited) + { + closelog(); + openlog("doldacond", LOG_PID, syslogfac); + } else { + openlog("doldacond", LOG_PID, syslogfac); + logtostderr = 1; + logtosyslog = 0; + inited = 1; + } +} diff --git a/daemon/log.h b/daemon/log.h new file mode 100644 index 0000000..b411418 --- /dev/null +++ b/daemon/log.h @@ -0,0 +1,36 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _LOG_H +#define _LOG_H + +#include + +#define LOGOOM(size) flog(LOG_CRIT, "%s (%s:%i): out of memory (alloc %i)", __FUNCTION__, __FILE__, __LINE__, (size)) + +extern int logtostderr, logtosyslog; +extern int syslogfac; + +void flog(int priority, char *format, ...) +#ifdef __GNUC__ + __attribute__ ((format (printf, 2, 3))) +#endif +; +void initlog(void); + +#endif diff --git a/daemon/main.c b/daemon/main.c new file mode 100644 index 0000000..299fd0d --- /dev/null +++ b/daemon/main.c @@ -0,0 +1,558 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "utils.h" +#include "log.h" +#include "conf.h" +#include "module.h" +#include "net.h" +#include "client.h" +#include "sysevents.h" +#include "auth.h" + +struct module *modchain = NULL; +static struct timer *timers = NULL; +static struct child *children = NULL; +volatile int running; +volatile int reinit; +static volatile int childrendone = 0; + +struct timer *timercallback(double at, void (*func)(int, void *), void *data) +{ + struct timer *new; + + new = smalloc(sizeof(*new)); + new->at = at; + new->func = func; + new->data = data; + new->next = timers; + new->prev = NULL; + if(timers != NULL) + timers->prev = new; + timers = new; + return(new); +} + +void canceltimer(struct timer *timer) +{ + if(timer->next != NULL) + timer->next->prev = timer->prev; + if(timer->prev != NULL) + timer->prev->next = timer->next; + if(timer == timers) + timers = timer->next; + timer->func(1, timer->data); + free(timer); +} + +void childcallback(pid_t pid, void (*func)(pid_t, int, void *), void *data) +{ + struct child *new; + + new = smalloc(sizeof(*new)); + new->pid = pid; + new->callback = func; + new->data = data; + new->finished = 0; + new->prev = NULL; + new->next = children; + if(children != NULL) + children->prev = new; + children = new; +} + +static void preinit(int hup) +{ + struct module *mod; + + for(mod = modchain; mod != NULL; mod = mod->next) + { + if(mod->preinit) + mod->preinit(hup); + if(!hup && ((mod->conf.vars != NULL) || (mod->conf.cmds != NULL))) + confregmod(&mod->conf); + } +} + +static void init(int hup) +{ + struct module *mod; + + for(mod = modchain; mod != NULL; mod = mod->next) + { + if(mod->init && mod->init(hup)) + { + flog(LOG_CRIT, "initialization of \"%s\" failed", mod->name); + exit(1); + } + } +} + +static void terminate(void) +{ + struct module *mod; + + for(mod = modchain; mod != NULL; mod = mod->next) + { + if(mod->terminate) + mod->terminate(); + } +} + +static void handler(int signum) +{ + pid_t pid; + int status; + struct child *child; + FILE *dumpfile; + extern int numfnetnodes, numtransfers, numdcpeers; + + switch(signum) + { + case SIGHUP: + reinit = 1; + break; + case SIGINT: + case SIGTERM: + running = 0; + break; + case SIGCHLD: + while((pid = waitpid(-1, &status, WNOHANG)) > 0) + { + for(child = children; child != NULL; child = child->next) + { + if(child->pid == pid) + { + child->finished = 1; + child->status = status; + } + } + childrendone = 1; + } + break; + case SIGUSR1: + flog(LOG_NOTICE, "forking and dumping core upon SIGUSR1"); + if(fork() == 0) + abort(); + break; + case SIGUSR2: + flog(LOG_NOTICE, "dumping memstats to /tmp/dc-mem upon SIGUSR2"); + if((dumpfile = fopen("/tmp/dc-mem", "w")) == NULL) { + flog(LOG_ERR, "could not dump stats: %s", strerror(errno)); + break; + } + fprintf(dumpfile, "%i %i %i\n", numfnetnodes, numtransfers, numdcpeers); + fclose(dumpfile); + break; + } +} + +pid_t forksess(uid_t user, struct authhandle *auth, void (*ccbfunc)(pid_t, int, void *), void *data, ...) +{ + int i, o; + int cpipe[2]; + struct + { + int tfd; + int fd; + } *files; + int maxfd, type, numfiles; + int acc; + int *ibuf; + struct passwd *pwent; + pid_t pid; + char *buf; + va_list args; + sigset_t sigset; + int ret, status; + + if((pwent = getpwuid(user)) == NULL) + { + flog(LOG_WARNING, "no passwd entry for uid %i, cannot fork session", user); + errno = EACCES; + return(-1); + } + if((geteuid() != 0) && (user != geteuid())) + { + flog(LOG_WARNING, "cannot fork non-owning session when not running as root (EUID is %i, target UID is %i)", geteuid(), user); + errno = EPERM; + return(-1); + } + va_start(args, data); + numfiles = 0; + files = NULL; + maxfd = 0; + while((type = va_arg(args, int)) != FD_END) + { + files = srealloc(files, sizeof(*files) * (numfiles + 1)); + files[numfiles].fd = va_arg(args, int); + if(files[numfiles].fd > maxfd) + maxfd = files[numfiles].fd; + acc = va_arg(args, int); + if(type == FD_PIPE) + { + if(pipe(cpipe) < 0) + { + flog(LOG_CRIT, "could not create pipe(!): %s", strerror(errno)); + for(i = 0; i < numfiles; i++) + close(files[i].tfd); + return(-1); + } + ibuf = va_arg(args, int *); + if(acc == O_WRONLY) + { + *ibuf = cpipe[1]; + files[numfiles].tfd = cpipe[0]; + } else { + *ibuf = cpipe[0]; + files[numfiles].tfd = cpipe[1]; + } + } else if(type == FD_FILE) { + buf = va_arg(args, char *); + if((files[numfiles].tfd = open(buf, acc)) < 0) + { + flog(LOG_CRIT, "could not open file \"%s\": %s", buf, strerror(errno)); + for(i = 0; i < numfiles; i++) + close(files[i].tfd); + return(-1); + } + } + if(files[numfiles].tfd > maxfd) + maxfd = files[numfiles].tfd; + numfiles++; + } + va_end(args); + sigemptyset(&sigset); + sigaddset(&sigset, SIGCHLD); + sigprocmask(SIG_BLOCK, &sigset, NULL); + if((pid = fork()) < 0) + { + flog(LOG_WARNING, "could not fork(!) in forksess(): %s", strerror(errno)); + for(i = 0; i < numfiles; i++) + close(files[i].tfd); + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + } + if(pid == 0) + { + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + signal(SIGPIPE, SIG_DFL); + signal(SIGCHLD, SIG_DFL); + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGHUP, SIG_DFL); + for(i = 0; i < numfiles; i++) + { + if(dup2(files[i].tfd, maxfd + i + 1) < 0) + exit(127); + files[i].tfd = maxfd + i + 1; + } + for(i = 0; i < numfiles; i++) + { + if(dup2(files[i].tfd, files[i].fd) < 0) + exit(127); + } + initlog(); + for(i = 0; i < FD_SETSIZE; i++) + { + if(i <= maxfd) + { + for(o = 0; o < numfiles; o++) + { + if(i == files[o].fd) + break; + } + if(o == numfiles) + close(i); + } else { + close(i); + } + } + setpgrp(); + signal(SIGHUP, SIG_IGN); + errno = 0; + if((authopensess(auth)) != AUTH_SUCCESS) + { + flog(LOG_WARNING, "could not open session for user %s: %s", pwent->pw_name, (errno == 0)?"Unknown error - should be logged above":strerror(errno)); + exit(127); + } + if((pid = fork()) < 0) + { + authclosesess(auth); + exit(127); + } + if(pid == 0) + { + if(geteuid() == 0) + { + if(initgroups(pwent->pw_name, pwent->pw_gid)) + { + flog(LOG_WARNING, "could not initgroups: %s", strerror(errno)); + exit(127); + } + if(setgid(pwent->pw_gid)) + { + flog(LOG_WARNING, "could not setgid: %s", strerror(errno)); + exit(127); + } + if(setuid(pwent->pw_uid)) + { + flog(LOG_WARNING, "could not setuid: %s", strerror(errno)); + exit(127); + } + } + putenv(sprintf2("HOME=%s", pwent->pw_dir)); + putenv(sprintf2("SHELL=%s", pwent->pw_shell)); + putenv(sprintf2("USER=%s", pwent->pw_name)); + putenv(sprintf2("LOGNAME=%s", pwent->pw_name)); + putenv(sprintf2("PATH=%s/bin:/usr/local/bin:/bin:/usr/bin", pwent->pw_dir)); + chdir(pwent->pw_dir); + return(0); + } + for(i = 0; i < numfiles; i++) + close(files[i].fd); + while(((ret = waitpid(pid, &status, 0)) != pid) && (ret >= 0)); + authclosesess(auth); + if(ret < 0) + { + flog(LOG_WARNING, "waitpid(%i) said \"%s\"", pid, strerror(errno)); + exit(127); + } + if(!WIFEXITED(status)) + exit(127); + exit(WEXITSTATUS(status)); + } + for(i = 0; i < numfiles; i++) + close(files[i].tfd); + if(files != NULL) + free(files); + if(ccbfunc != NULL) + childcallback(pid, ccbfunc, data); + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + return(pid); +} + +int main(int argc, char **argv) +{ + int c; + int nofork; + char *configfile; + char *pidfile; + FILE *pfstream, *confstream; + int delay; + struct module *mod; + struct timer *timer, *ntimer; + struct child *child; + double now; + + nofork = 0; + syslogfac = LOG_DAEMON; + configfile = NULL; + pidfile = NULL; + while((c = getopt(argc, argv, "p:C:f:hn")) != -1) + { + switch(c) + { + case 'p': + pidfile = optarg; + break; + case 'C': + configfile = optarg; + break; + case 'f': + if(!strcmp(optarg, "auth")) + syslogfac = LOG_AUTH; + else if(!strcmp(optarg, "authpriv")) + syslogfac = LOG_AUTHPRIV; + else if(!strcmp(optarg, "cron")) + syslogfac = LOG_CRON; + else if(!strcmp(optarg, "daemon")) + syslogfac = LOG_DAEMON; + else if(!strcmp(optarg, "ftp")) + syslogfac = LOG_FTP; + else if(!strcmp(optarg, "kern")) + syslogfac = LOG_KERN; + else if(!strcmp(optarg, "lpr")) + syslogfac = LOG_LPR; + else if(!strcmp(optarg, "mail")) + syslogfac = LOG_MAIL; + else if(!strcmp(optarg, "news")) + syslogfac = LOG_NEWS; + else if(!strcmp(optarg, "syslog")) + syslogfac = LOG_SYSLOG; + else if(!strcmp(optarg, "user")) + syslogfac = LOG_USER; + else if(!strcmp(optarg, "uucp")) + syslogfac = LOG_UUCP; + else if(!strncmp(optarg, "local", 5) && (strlen(optarg) == 6)) + syslogfac = LOG_LOCAL0 + (optarg[5] - '0'); + else + fprintf(stderr, "unknown syslog facility %s, using daemon\n", optarg); + break; + case 'n': + nofork = 1; + break; + case 'h': + case ':': + case '?': + default: + printf("usage: doldacond [-hn] [-C configfile] [-p pidfile] [-f facility]\n"); + exit(c != 'h'); + } + } + setlocale(LC_ALL, ""); + initlog(); + signal(SIGPIPE, SIG_IGN); + signal(SIGHUP, handler); + signal(SIGINT, handler); + signal(SIGTERM, handler); + signal(SIGCHLD, handler); + signal(SIGUSR1, handler); + signal(SIGUSR2, handler); + preinit(0); + if(configfile == NULL) + { + if((configfile = findconfigfile()) == NULL) + { + flog(LOG_CRIT, "could not find a configuration file"); + exit(1); + } + } + pfstream = NULL; + if(pidfile != NULL) + { + if((pfstream = fopen(pidfile, "w")) == NULL) + { + flog(LOG_CRIT, "could not open specified PID file %s: %s", pidfile, strerror(errno)); + exit(1); + } + } + if((confstream = fopen(configfile, "r")) == NULL) + { + flog(LOG_CRIT, "could not open configuration file %s: %s", configfile, strerror(errno)); + exit(1); + } + readconfig(confstream); + fclose(confstream); + init(0); + if(!nofork) + { + logtosyslog = 1; + daemon(0, 0); + flog(LOG_INFO, "daemonized"); + logtostderr = 0; + } + if(pfstream != NULL) { + fprintf(pfstream, "%i\n", getpid()); + fclose(pfstream); + } + running = 1; + reinit = 0; + while(running) + { + if(reinit) + { + if((confstream = fopen(configfile, "r")) == NULL) + { + flog(LOG_ERR, "could not open configuration file %s: %s (ignoring HUP)", configfile, strerror(errno)); + } else { + preinit(1); + readconfig(confstream); + fclose(confstream); + init(1); + } + reinit = 0; + } + delay = 1000; /* -1; */ + for(mod = modchain; mod != NULL; mod = mod->next) + { + if(mod->run && mod->run()) + delay = 0; + } + if(!running) + delay = 0; + if(delay != 0) + { + now = ntime(); + for(timer = timers; timer != NULL; timer = timer->next) + { + if((delay == -1) || ((int)((timer->at - now) * 1000.0) < delay)) + delay = (int)((timer->at - now) * 1000.0); + } + } + if(childrendone) + { + delay = 0; + childrendone = 0; + } + pollsocks(delay); + now = ntime(); + for(timer = timers; timer != NULL; timer = ntimer) + { + ntimer = timer->next; + if(now < timer->at) + continue; + if(timer->prev != NULL) + timer->prev->next = timer->next; + if(timer->next != NULL) + timer->next->prev = timer->prev; + if(timer == timers) + timers = timer->next; + timer->func(0, timer->data); + free(timer); + } + do + { + for(child = children; child != NULL; child = child->next) + { + if(child->finished) + { + child->callback(child->pid, child->status, child->data); + if(child == children) + children = child->next; + if(child->prev != NULL) + child->prev->next = child->next; + if(child->next != NULL) + child->next->prev = child->prev; + free(child); + break; + } + } + } while(child != NULL); + } + flog(LOG_INFO, "terminating..."); + terminate(); + return(0); +} diff --git a/daemon/module.h b/daemon/module.h new file mode 100644 index 0000000..385f6c9 --- /dev/null +++ b/daemon/module.h @@ -0,0 +1,65 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _MODULE_H +#define _MODULE_H + +#include + +#include "conf.h" + +struct module +{ + struct module *next; + char *name; + /* Called before the configuration file is read. Must either + * succeed or call exit. Note that it is called both at startup + * and when SIGHUP has been received (then with hup = 1). */ + void (*preinit)(int hup); + /* Called when the configuration file has been read. Return zero + * on success and non-zero on failure. Note, as with preinit, that + * it can be called both at startup and after SIHUP. */ + int (*init)(int hup); + /* Called every "cycle". Its return value determines whether the + * module still has work to do, and thus determines whether the + * next pollsocks should block or not. Return non-zero whenever + * the module has more work to do. */ + int (*run)(void); + /* Called when the daemon is shutting down. */ + void (*terminate)(void); + struct configmod conf; +}; + +#define MODULE(mod) \ +static void __attribute__ ((constructor)) __regmod(void) \ +{ \ + extern struct module *modchain; \ + \ + if(mod.name == NULL) \ + { \ + fprintf(stderr, "module at %p has no name", &mod); \ + exit(1); \ + } \ + mod.conf.name = mod.name; \ + mod.next = modchain; \ + modchain = &mod; \ +} + +void regmod(struct module *); + +#endif diff --git a/daemon/net.c b/daemon/net.c new file mode 100644 index 0000000..f16441f --- /dev/null +++ b/daemon/net.c @@ -0,0 +1,1124 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +/* XXX: Implement SOCKS proxyability */ + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_LINUX_SOCKIOS_H +#include +#endif +#include +#include + +#include "conf.h" +#include "net.h" +#include "module.h" +#include "log.h" +#include "utils.h" +#include "sysevents.h" + +static struct configvar myvars[] = +{ + /* 0 = Direct mode, 1 = Passive mode, 2 = SOCKS proxy */ + {CONF_VAR_INT, "mode", {.num = 0}}, + /* Only for direct mode */ + {CONF_VAR_IPV4, "visibleipv4", {.ipv4 = {0}}}, + {CONF_VAR_STRING, "publicif", {.str = L""}}, + {CONF_VAR_END} +}; + +static struct socket *sockets = NULL; +int numsocks = 0; + +/* XXX: Get autoconf for all this... */ +int getpublicaddr(int af, struct sockaddr **addr, socklen_t *lenbuf) +{ + struct sockaddr_in *ipv4; + struct configvar *var; + void *bufend; + int sock; + struct ifconf conf; + struct ifreq *ifr, req; + char *pif; + + if(af == AF_INET) + { + var = confgetvar("net", "visibleipv4"); + if(var->val.ipv4.s_addr != 0) + { + ipv4 = smalloc(sizeof(*ipv4)); + ipv4->sin_family = AF_INET; + ipv4->sin_addr.s_addr = var->val.ipv4.s_addr; + *addr = (struct sockaddr *)ipv4; + *lenbuf = sizeof(*ipv4); + return(0); + } + if((pif = icwcstombs(confgetstr("net", "publicif"), NULL)) == NULL) + { + flog(LOG_ERR, "could not convert net.publicif into local charset: %s", strerror(errno)); + return(-1); + } + if((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) + return(-1); + conf.ifc_buf = smalloc(conf.ifc_len = 65536); + if(ioctl(sock, SIOCGIFCONF, &conf) < 0) + { + free(conf.ifc_buf); + close(sock); + return(-1); + } + bufend = ((char *)conf.ifc_buf) + conf.ifc_len; + ipv4 = NULL; + for(ifr = conf.ifc_ifcu.ifcu_req; (void *)ifr < bufend; ifr++) + { + memset(&req, 0, sizeof(req)); + memcpy(req.ifr_name, ifr->ifr_name, sizeof(ifr->ifr_name)); + if(ioctl(sock, SIOCGIFFLAGS, &req) < 0) + { + free(conf.ifc_buf); + close(sock); + return(-1); + } + if(!(req.ifr_flags & IFF_UP)) + continue; + if(ifr->ifr_addr.sa_family == AF_INET) + { + if(ntohl(((struct sockaddr_in *)&ifr->ifr_addr)->sin_addr.s_addr) == 0x7f000001) + continue; + if(ipv4 == NULL) + { + ipv4 = smalloc(sizeof(*ipv4)); + memcpy(ipv4, &ifr->ifr_addr, sizeof(ifr->ifr_addr)); + } else { + free(ipv4); + flog(LOG_WARNING, "could not locate an unambiguous interface for determining your public IP address - set net.publicif"); + errno = ENFILE; /* XXX: There's no appropriate one for this... */ + return(-1); + } + } + } + close(sock); + if(ipv4 != NULL) + { + *addr = (struct sockaddr *)ipv4; + *lenbuf = sizeof(*ipv4); + return(0); + } + errno = ENETDOWN; + return(-1); + } + errno = EPFNOSUPPORT; + return(-1); +} + +static struct socket *newsock(int type) +{ + struct socket *new; + + new = smalloc(sizeof(*new)); + new->refcount = 2; + new->fd = -1; + new->isrealsocket = 1; + new->family = -1; + new->tos = 0; + new->type = type; + new->state = -1; + new->ignread = 0; + new->close = 0; + new->remote = NULL; + new->remotelen = 0; + switch(type) + { + case SOCK_STREAM: + new->outbuf.s.buf = NULL; + new->outbuf.s.bufsize = 0; + new->outbuf.s.datasize = 0; + new->inbuf.s.buf = NULL; + new->inbuf.s.bufsize = 0; + new->inbuf.s.datasize = 0; + break; + case SOCK_DGRAM: + new->outbuf.d.f = new->outbuf.d.l = NULL; + new->inbuf.d.f = new->inbuf.d.l = NULL; + break; + } + new->conncb = NULL; + new->errcb = NULL; + new->readcb = NULL; + new->writecb = NULL; + new->acceptcb = NULL; + new->next = sockets; + new->prev = NULL; + if(sockets != NULL) + sockets->prev = new; + sockets = new; + numsocks++; + return(new); +} + +static struct socket *mksock(int domain, int type) +{ + int fd; + struct socket *new; + + if((fd = socket(domain, type, 0)) < 0) + { + flog(LOG_CRIT, "could not create socket: %s", strerror(errno)); + return(NULL); + } + new = newsock(type); + new->fd = fd; + new->family = domain; + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + return(new); +} + +struct socket *wrapsock(int fd) +{ + struct socket *new; + + new = newsock(SOCK_STREAM); + new->fd = fd; + new->state = SOCK_EST; + new->isrealsocket = 0; + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + return(new); +} + +static void unlinksock(struct socket *sk) +{ + if(sk->prev != NULL) + sk->prev->next = sk->next; + if(sk->next != NULL) + sk->next->prev = sk->prev; + if(sk == sockets) + sockets = sk->next; + putsock(sk); + numsocks--; +} + +void getsock(struct socket *sk) +{ + sk->refcount++; +} + +void putsock(struct socket *sk) +{ + struct dgrambuf *buf; + + if(--(sk->refcount) == 0) + { + switch(sk->type) + { + case SOCK_STREAM: + if(sk->outbuf.s.buf != NULL) + free(sk->outbuf.s.buf); + if(sk->inbuf.s.buf != NULL) + free(sk->inbuf.s.buf); + break; + case SOCK_DGRAM: + while((buf = sk->outbuf.d.f) != NULL) + { + sk->outbuf.d.f = buf->next; + free(buf->data); + free(buf); + } + while((buf = sk->inbuf.d.f) != NULL) + { + sk->inbuf.d.f = buf->next; + free(buf->data); + free(buf); + } + break; + } + if(sk->fd >= 0) + close(sk->fd); + if(sk->remote != NULL) + free(sk->remote); + free(sk); + } +} + +void *sockgetinbuf(struct socket *sk, size_t *size) +{ + void *buf; + struct dgrambuf *dbuf; + + switch(sk->type) + { + case SOCK_STREAM: + if((sk->inbuf.s.buf == NULL) || (sk->inbuf.s.datasize == 0)) + { + *size = 0; + return(NULL); + } + buf = sk->inbuf.s.buf; + *size = sk->inbuf.s.datasize; + sk->inbuf.s.buf = NULL; + sk->inbuf.s.bufsize = sk->inbuf.s.datasize = 0; + return(buf); + case SOCK_DGRAM: + if((dbuf = sk->inbuf.d.f) == NULL) + return(NULL); + sk->inbuf.d.f = dbuf->next; + if(dbuf->next == NULL) + sk->inbuf.d.l = NULL; + buf = dbuf->data; + *size = dbuf->size; + free(dbuf->addr); + free(dbuf); + return(buf); + } + return(NULL); +} + +static void sockrecv(struct socket *sk) +{ + int ret, inq; + struct dgrambuf *dbuf; + + switch(sk->type) + { + case SOCK_STREAM: +#if defined(HAVE_LINUX_SOCKIOS_H) && defined(SIOCINQ) + /* SIOCINQ is Linux-specific AFAIK, but I really have no idea + * how to read the inqueue size on other OSs */ + if(ioctl(sk->fd, SIOCINQ, &inq)) + { + /* I don't really know what could go wrong here, so let's + * assume it's transient. */ + flog(LOG_WARNING, "SIOCINQ return %s on socket %i, falling back to 2048 bytes", strerror(errno), sk->fd); + inq = 2048; + } +#else + inq = 2048; +#endif + if(inq > 65536) + inq = 65536; + sizebuf(&sk->inbuf.s.buf, &sk->inbuf.s.bufsize, sk->inbuf.s.datasize + inq, 1, 1); + ret = read(sk->fd, sk->inbuf.s.buf + sk->inbuf.s.datasize, inq); + if(ret < 0) + { + if((errno == EINTR) || (errno == EAGAIN)) + return; + if(sk->errcb != NULL) + sk->errcb(sk, errno, sk->data); + closesock(sk); + return; + } + if(ret == 0) + { + if(sk->errcb != NULL) + sk->errcb(sk, 0, sk->data); + closesock(sk); + return; + } + sk->inbuf.s.datasize += ret; + if(sk->readcb != NULL) + sk->readcb(sk, sk->data); + break; + case SOCK_DGRAM: + if(ioctl(sk->fd, SIOCINQ, &inq)) + { + /* I don't really know what could go wrong here, so let's + * assume it's transient. */ + flog(LOG_WARNING, "SIOCINQ return %s on socket %i", strerror(errno), sk->fd); + return; + } + dbuf = smalloc(sizeof(*dbuf)); + dbuf->data = smalloc(inq); + dbuf->addr = smalloc(dbuf->addrlen = sizeof(struct sockaddr_storage)); + ret = recvfrom(sk->fd, dbuf->data, inq, 0, dbuf->addr, &dbuf->addrlen); + if(ret < 0) + { + free(dbuf->addr); + free(dbuf->data); + free(dbuf); + if((errno == EINTR) || (errno == EAGAIN)) + return; + if(sk->errcb != NULL) + sk->errcb(sk, errno, sk->data); + closesock(sk); + return; + } + /* On UDP/IPv[46], ret == 0 doesn't mean EOF (since UDP can't + * have EOF), but rather an empty packet. I don't know if any + * other potential DGRAM protocols might have an EOF + * condition, so let's play safe. */ + if(ret == 0) + { + free(dbuf->addr); + free(dbuf->data); + free(dbuf); + if(!((sk->family == AF_INET) || (sk->family == AF_INET6))) + { + if(sk->errcb != NULL) + sk->errcb(sk, 0, sk->data); + closesock(sk); + } + return; + } + dbuf->addr = srealloc(dbuf->addr, dbuf->addrlen); + dbuf->data = srealloc(dbuf->data, dbuf->size = ret); + dbuf->next = NULL; + if(sk->inbuf.d.l != NULL) + sk->inbuf.d.l->next = dbuf; + else + sk->inbuf.d.f = dbuf; + sk->inbuf.d.l = dbuf; + if(sk->readcb != NULL) + sk->readcb(sk, sk->data); + break; + } +} + +static void sockflush(struct socket *sk) +{ + int ret; + struct dgrambuf *dbuf; + + switch(sk->type) + { + case SOCK_STREAM: + if(sk->isrealsocket) + ret = send(sk->fd, sk->outbuf.s.buf, sk->outbuf.s.datasize, MSG_DONTWAIT | MSG_NOSIGNAL); + else + ret = write(sk->fd, sk->outbuf.s.buf, sk->outbuf.s.datasize); + if(ret < 0) + { + /* For now, assume transient error, since + * the socket is polled for errors */ + break; + } + if(ret > 0) + { + memmove(sk->outbuf.s.buf, ((char *)sk->outbuf.s.buf) + ret, sk->outbuf.s.datasize -= ret); + if(sk->writecb != NULL) + sk->writecb(sk, sk->data); + } + break; + case SOCK_DGRAM: + dbuf = sk->outbuf.d.f; + if((sk->outbuf.d.f = dbuf->next) == NULL) + sk->outbuf.d.l = NULL; + sendto(sk->fd, dbuf->data, dbuf->size, MSG_DONTWAIT | MSG_NOSIGNAL, dbuf->addr, dbuf->addrlen); + free(dbuf->data); + free(dbuf->addr); + free(dbuf); + if(sk->writecb != NULL) + sk->writecb(sk, sk->data); + break; + } +} + +void closesock(struct socket *sk) +{ + sk->state = SOCK_STL; + close(sk->fd); + sk->fd = -1; + sk->close = 0; +} + +void sockqueue(struct socket *sk, void *data, size_t size) +{ + struct dgrambuf *new; + + if(sk->state == SOCK_STL) + return; + switch(sk->type) + { + case SOCK_STREAM: + sizebuf(&(sk->outbuf.s.buf), &(sk->outbuf.s.bufsize), sk->outbuf.s.datasize + size, 1, 1); + memcpy(sk->outbuf.s.buf + sk->outbuf.s.datasize, data, size); + sk->outbuf.s.datasize += size; + break; + case SOCK_DGRAM: + if(sk->remote == NULL) + return; + new = smalloc(sizeof(*new)); + new->next = NULL; + memcpy(new->data = smalloc(size), data, new->size = size); + memcpy(new->addr = smalloc(sk->remotelen), sk->remote, new->addrlen = sk->remotelen); + if(sk->outbuf.d.l == NULL) + { + sk->outbuf.d.l = sk->outbuf.d.f = new; + } else { + sk->outbuf.d.l->next = new; + sk->outbuf.d.l = new; + } + break; + } +} + +size_t sockgetdatalen(struct socket *sk) +{ + struct dgrambuf *b; + size_t ret; + + switch(sk->type) + { + case SOCK_STREAM: + ret = sk->inbuf.s.datasize; + break; + case SOCK_DGRAM: + ret = 0; + for(b = sk->inbuf.d.f; b != NULL; b = b->next) + ret += b->size; + break; + } + return(ret); +} + +size_t sockqueuesize(struct socket *sk) +{ + struct dgrambuf *b; + size_t ret; + + switch(sk->type) + { + case SOCK_STREAM: + ret = sk->outbuf.s.datasize; + break; + case SOCK_DGRAM: + ret = 0; + for(b = sk->outbuf.d.f; b != NULL; b = b->next) + ret += b->size; + break; + } + return(ret); +} + +struct socket *netcslisten(int type, struct sockaddr *name, socklen_t namelen, void (*func)(struct socket *, struct socket *, void *), void *data) +{ + struct socket *sk; + + if(confgetint("net", "mode") == 1) + { + errno = EOPNOTSUPP; + return(NULL); + } + /* I don't know if this is actually correct (it probably isn't), + * but since, at on least Linux systems, PF_* are specifically + * #define'd to their AF_* counterparts, it allows for a severely + * smoother implementation. If it breaks something on your + * platform, please tell me so. + */ + if(confgetint("net", "mode") == 0) + { + if((sk = mksock(name->sa_family, type)) == NULL) + return(NULL); + sk->state = SOCK_LST; + if(bind(sk->fd, name, namelen) < 0) + { + putsock(sk); + return(NULL); + } + if(listen(sk->fd, 16) < 0) + { + putsock(sk); + return(NULL); + } + sk->acceptcb = func; + sk->data = data; + return(sk); + } + errno = EOPNOTSUPP; + return(NULL); +} + +/* + * The difference between netcslisten() and netcslistenlocal() is that + * netcslistenlocal() always listens on the local host, instead of + * following proxy/passive mode directions. It is suitable for eg. the + * UI channel, while the file sharing networks should, naturally, use + * netcslisten() instead. +*/ + +struct socket *netcslistenlocal(int type, struct sockaddr *name, socklen_t namelen, void (*func)(struct socket *, struct socket *, void *), void *data) +{ + struct socket *sk; + + /* I don't know if this is actually correct (it probably isn't), + * but since, at on least Linux systems, PF_* are specifically + * #define'd to their AF_* counterparts, it allows for a severely + * smoother implementation. If it breaks something on your + * platform, please tell me so. + */ + if((sk = mksock(name->sa_family, type)) == NULL) + return(NULL); + sk->state = SOCK_LST; + if(bind(sk->fd, name, namelen) < 0) + { + putsock(sk); + return(NULL); + } + if(listen(sk->fd, 16) < 0) + { + putsock(sk); + return(NULL); + } + sk->acceptcb = func; + sk->data = data; + return(sk); +} + +struct socket *netcsdgram(struct sockaddr *name, socklen_t namelen) +{ + struct socket *sk; + int mode; + + mode = confgetint("net", "mode"); + if((mode == 0) || (mode == 1)) + { + if((sk = mksock(name->sa_family, SOCK_DGRAM)) == NULL) + return(NULL); + if(bind(sk->fd, name, namelen) < 0) + { + putsock(sk); + return(NULL); + } + sk->state = SOCK_EST; + return(sk); + } + errno = EOPNOTSUPP; + return(NULL); +} + +struct socket *netdupsock(struct socket *sk) +{ + struct socket *newsk; + + newsk = newsock(sk->type); + if((newsk->fd = dup(sk->fd)) < 0) + { + flog(LOG_WARNING, "could not dup() socket: %s", strerror(errno)); + putsock(newsk); + return(NULL); + } + newsk->state = sk->state; + newsk->ignread = sk->ignread; + if(sk->remote != NULL) + memcpy(newsk->remote = smalloc(sk->remotelen), sk->remote, newsk->remotelen = sk->remotelen); + return(newsk); +} + +void netdgramconn(struct socket *sk, struct sockaddr *addr, socklen_t addrlen) +{ + if(sk->remote != NULL) + free(sk->remote); + memcpy(sk->remote = smalloc(addrlen), addr, sk->remotelen = addrlen); + sk->ignread = 1; +} + +struct socket *netcsconn(struct sockaddr *addr, socklen_t addrlen, void (*func)(struct socket *, int, void *), void *data) +{ + struct socket *sk; + int mode; + + mode = confgetint("net", "mode"); + if((mode == 0) || (mode == 1)) + { + if((sk = mksock(addr->sa_family, SOCK_STREAM)) == NULL) + return(NULL); + memcpy(sk->remote = smalloc(addrlen), addr, sk->remotelen = addrlen); + if(!connect(sk->fd, addr, addrlen)) + { + sk->state = SOCK_EST; + func(sk, 0, data); + return(sk); + } + if(errno == EINPROGRESS) + { + sk->state = SOCK_SYN; + sk->conncb = func; + sk->data = data; + return(sk); + } + putsock(sk); + return(NULL); + } + errno = EOPNOTSUPP; + return(NULL); +} + +int pollsocks(int timeout) +{ + int i, num, ret, retlen; + int newfd; + struct pollfd *pfds; + struct socket *sk, *next, *newsk; + struct sockaddr_storage ss; + socklen_t sslen; + + pfds = smalloc(sizeof(*pfds) * (num = numsocks)); + for(i = 0, sk = sockets; i < num; sk = sk->next) + { + if(sk->state == SOCK_STL) + { + num--; + continue; + } + pfds[i].fd = sk->fd; + pfds[i].events = 0; + if(!sk->ignread) + pfds[i].events |= POLLIN; + if((sk->state == SOCK_SYN) || (sockqueuesize(sk) > 0)) + pfds[i].events |= POLLOUT; + pfds[i].revents = 0; + i++; + } + ret = poll(pfds, num, timeout); + if(ret < 0) + { + if(errno != EINTR) + { + flog(LOG_CRIT, "pollsocks: poll errored out: %s", strerror(errno)); + /* To avoid CPU hogging in case it's bad, which it + * probably is. */ + sleep(1); + } + free(pfds); + return(1); + } + for(sk = sockets; sk != NULL; sk = next) + { + next = sk->next; + for(i = 0; i < num; i++) + { + if(pfds[i].fd == sk->fd) + break; + } + if(i == num) + continue; + switch(sk->state) + { + case SOCK_LST: + if(pfds[i].revents & POLLIN) + { + sslen = sizeof(ss); + if((newfd = accept(sk->fd, (struct sockaddr *)&ss, &sslen)) < 0) + { + if(sk->errcb != NULL) + sk->errcb(sk, errno, sk->data); + } + newsk = newsock(sk->type); + newsk->fd = newfd; + newsk->family = sk->family; + newsk->state = SOCK_EST; + memcpy(newsk->remote = smalloc(sslen), &ss, sslen); + newsk->remotelen = sslen; + putsock(newsk); + if(sk->acceptcb != NULL) + sk->acceptcb(sk, newsk, sk->data); + } + if(pfds[i].revents & POLLERR) + { + retlen = sizeof(ret); + getsockopt(sk->fd, SOL_SOCKET, SO_ERROR, &ret, &retlen); + if(sk->errcb != NULL) + sk->errcb(sk, ret, sk->data); + continue; + } + break; + case SOCK_SYN: + if(pfds[i].revents & POLLERR) + { + retlen = sizeof(ret); + getsockopt(sk->fd, SOL_SOCKET, SO_ERROR, &ret, &retlen); + if(sk->conncb != NULL) + sk->conncb(sk, ret, sk->data); + closesock(sk); + continue; + } + if(pfds[i].revents & (POLLIN | POLLOUT)) + { + sk->state = SOCK_EST; + if(sk->conncb != NULL) + sk->conncb(sk, 0, sk->data); + } + break; + case SOCK_EST: + if(pfds[i].revents & POLLERR) + { + retlen = sizeof(ret); + getsockopt(sk->fd, SOL_SOCKET, SO_ERROR, &ret, &retlen); + if(sk->errcb != NULL) + sk->errcb(sk, ret, sk->data); + closesock(sk); + continue; + } + if(pfds[i].revents & POLLIN) + sockrecv(sk); + if(pfds[i].revents & POLLOUT) + { + if(sockqueuesize(sk) > 0) + sockflush(sk); + } + break; + } + if(pfds[i].revents & POLLNVAL) + { + flog(LOG_CRIT, "BUG: stale socket struct on fd %i", sk->fd); + sk->state = SOCK_STL; + unlinksock(sk); + continue; + } + if(pfds[i].revents & POLLHUP) + { + if(sk->errcb != NULL) + sk->errcb(sk, 0, sk->data); + closesock(sk); + unlinksock(sk); + continue; + } + } + free(pfds); + for(sk = sockets; sk != NULL; sk = next) + { + next = sk->next; + if(sk->refcount == 1 && (sockqueuesize(sk) == 0)) + { + unlinksock(sk); + continue; + } + if(sk->close && (sockqueuesize(sk) == 0)) + closesock(sk); + if(sk->state == SOCK_STL) + { + unlinksock(sk); + continue; + } + } + return(1); +} + +int socksettos(struct socket *sk, int tos) +{ + if(sk->family == AF_INET) + { + if(setsockopt(sk->fd, SOL_IP, IP_TOS, &tos, sizeof(tos)) < 0) + { + flog(LOG_WARNING, "could not set sock TOS to %i: %s", tos, strerror(errno)); + return(-1); + } + return(0); + } + /* XXX: How does the IPv6 traffic class work? */ + flog(LOG_WARNING, "could not set TOS on sock of family %i", sk->family); + return(1); +} + +struct resolvedata +{ + int fd; + void (*callback)(struct sockaddr *addr, int addrlen, void *data); + void *data; + struct sockaddr_storage addr; + int addrlen; +}; + +static void resolvecb(pid_t pid, int status, struct resolvedata *data) +{ + static char buf[80]; + int ret; + struct sockaddr_in *ipv4; + + if(!status) + { + if((ret = read(data->fd, buf, sizeof(buf))) != 4) + { + errno = ENONET; + data->callback(NULL, 0, data->data); + } else { + ipv4 = (struct sockaddr_in *)&data->addr; + memcpy(&ipv4->sin_addr, buf, 4); + data->callback((struct sockaddr *)ipv4, sizeof(*ipv4), data->data); + } + } else { + errno = ENONET; + data->callback(NULL, 0, data->data); + } + close(data->fd); + free(data); +} + +int netresolve(char *addr, void (*callback)(struct sockaddr *addr, int addrlen, void *data), void *data) +{ + int i; + char *p; + int port; + int pfd[2]; + pid_t child; + struct resolvedata *rdata; + struct sockaddr_in ipv4; + struct hostent *he; + sigset_t sigset; + + /* IPv4 */ + port = -1; + if((p = strchr(addr, ':')) != NULL) + { + *p = 0; + port = atoi(p + 1); + } + ipv4.sin_family = AF_INET; + ipv4.sin_port = htons(port); + if(inet_aton(addr, &ipv4.sin_addr)) + { + callback((struct sockaddr *)&ipv4, sizeof(ipv4), data); + } else { + sigemptyset(&sigset); + sigaddset(&sigset, SIGCHLD); + sigprocmask(SIG_BLOCK, &sigset, NULL); + if((pipe(pfd) < 0) || ((child = fork()) < 0)) + { + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + return(-1); + } + if(child == 0) + { + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + for(i = 3; i < FD_SETSIZE; i++) + { + if(i != pfd[1]) + close(i); + } + signal(SIGALRM, SIG_DFL); + alarm(30); + if((he = gethostbyname(addr)) == NULL) + exit(1); + write(pfd[1], he->h_addr_list[0], 4); + exit(0); + } else { + close(pfd[1]); + fcntl(pfd[0], F_SETFL, fcntl(pfd[0], F_GETFL) | O_NONBLOCK); + rdata = smalloc(sizeof(*rdata)); + rdata->fd = pfd[0]; + rdata->callback = callback; + rdata->data = data; + memcpy(&rdata->addr, &ipv4, rdata->addrlen = sizeof(ipv4)); + childcallback(child, (void (*)(pid_t, int, void *))resolvecb, rdata); + sigprocmask(SIG_UNBLOCK, &sigset, NULL); + return(1); + } + } + return(0); +} + +int sockgetlocalname(struct socket *sk, struct sockaddr **namebuf, socklen_t *lenbuf) +{ + socklen_t len; + struct sockaddr_storage name; + + *namebuf = NULL; + if((sk->state == SOCK_STL) || (sk->fd < 0)) + return(-1); + len = sizeof(name); + if(getsockname(sk->fd, (struct sockaddr *)&name, &len) < 0) + { + flog(LOG_ERR, "BUG: alive socket with dead fd in sockgetlocalname"); + return(-1); + } + *namebuf = memcpy(smalloc(len), &name, len); + *lenbuf = len; + return(0); +} + +int sockgetremotename(struct socket *sk, struct sockaddr **namebuf, socklen_t *lenbuf) +{ + socklen_t len; + struct sockaddr_storage name; + struct sockaddr_in *ipv4; + struct sockaddr *pname; + socklen_t pnamelen; + + switch(confgetint("net", "mode")) + { + case 0: + *namebuf = NULL; + if((sk->state == SOCK_STL) || (sk->fd < 0)) + return(-1); + len = sizeof(name); + if(getsockname(sk->fd, (struct sockaddr *)&name, &len) < 0) + { + flog(LOG_ERR, "BUG: alive socket with dead fd in sockgetremotename"); + return(-1); + } + if(name.ss_family == AF_INET) + { + ipv4 = (struct sockaddr_in *)&name; + if(getpublicaddr(AF_INET, &pname, &pnamelen) < 0) + { + flog(LOG_WARNING, "could not determine public IP address - strange things may happen"); + return(-1); + } + ipv4->sin_addr.s_addr = ((struct sockaddr_in *)pname)->sin_addr.s_addr; + free(pname); + } + *namebuf = memcpy(smalloc(len), &name, len); + *lenbuf = len; + return(0); + case 1: + errno = EOPNOTSUPP; + return(-1); + default: + flog(LOG_CRIT, "unknown net mode %i active", confgetint("net", "mode")); + errno = EOPNOTSUPP; + return(-1); + } +} + +char *formataddress(struct sockaddr *arg, socklen_t arglen) +{ + struct sockaddr_un *UNIX; /* Some wise guy has #defined unix with + * lowercase letters to 1, so I do this + * instead. */ + struct sockaddr_in *ipv4; +#ifdef HAVE_IPV6 + struct sockaddr_in6 *ipv6; +#endif + static char *ret = NULL; + char buf[1024]; + + if(ret != NULL) + free(ret); + ret = NULL; + switch(arg->sa_family) + { + case AF_UNIX: + UNIX = (struct sockaddr_un *)arg; + ret = sprintf2("%s", UNIX->sun_path); + break; + case AF_INET: + ipv4 = (struct sockaddr_in *)arg; + if(inet_ntop(AF_INET, &ipv4->sin_addr, buf, sizeof(buf)) == NULL) + return(NULL); + ret = sprintf2("%s:%i", buf, (int)ntohs(ipv4->sin_port)); + break; +#ifdef HAVE_IPV6 + case AF_INET6: + ipv6 = (struct sockaddr_in6 *)arg; + if(inet_ntop(AF_INET6, &ipv6->sin6_addr, buf, sizeof(buf)) == NULL) + return(NULL); + ret = sprintf2("%s:%i", buf, (int)ntohs(ipv6->sin6_port)); + break; +#endif + default: + errno = EPFNOSUPPORT; + break; + } + return(ret); +} + +#if 0 + +/* + * It was very nice to use this, but it seems + * to mess things up, so I guess it has to go... :-( + */ + +static int formataddress(FILE *stream, const struct printf_info *info, const void *const *args) +{ + struct sockaddr *arg; + socklen_t arglen; + struct sockaddr_un *UNIX; /* Some wise guy has #defined unix with + * lowercase letters to 1, so I do this + * instead. */ + struct sockaddr_in *ipv4; + int ret; + + arg = *(struct sockaddr **)(args[0]); + arglen = *(socklen_t *)(args[1]); + switch(arg->sa_family) + { + case AF_UNIX: + UNIX = (struct sockaddr_un *)arg; + ret = fprintf(stream, "%s", UNIX->sun_path); + break; + case AF_INET: + ipv4 = (struct sockaddr_in *)arg; + ret = fprintf(stream, "%s:%i", inet_ntoa(ipv4->sin_addr), (int)ntohs(ipv4->sin_port)); + break; + default: + ret = -1; + errno = EPFNOSUPPORT; + break; + } + return(ret); +} + +static int formataddress_arginfo(const struct printf_info *info, size_t n, int *argtypes) +{ + if(n > 0) + argtypes[0] = PA_POINTER; + if(n > 1) + argtypes[1] = PA_INT; /* Sources tell me that socklen_t _must_ + * be an int, so I guess this should be + * safe. */ + return(2); +} +#endif + +static int init(int hup) +{ + if(!hup) + { + /* + if(register_printf_function('N', formataddress, formataddress_arginfo)) + { + flog(LOG_CRIT, "could not register printf handler %%N: %s", strerror(errno)); + return(1); + } + */ + } + return(0); +} + +static void terminate(void) +{ + while(sockets != NULL) + unlinksock(sockets); +} + +static struct module me = +{ + .name = "net", + .conf = + { + .vars = myvars + }, + .init = init, + .terminate = terminate +}; + +MODULE(me) diff --git a/daemon/net.h b/daemon/net.h new file mode 100644 index 0000000..451c398 --- /dev/null +++ b/daemon/net.h @@ -0,0 +1,113 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _NET_H +#define _NET_H + +#include + +#define SOCK_LST 0 /* Listening */ +#define SOCK_SYN 1 /* Connecting */ +#define SOCK_EST 2 /* Established */ +#define SOCK_STL 3 /* Stale, dead */ +#define SOCK_TOS_MINDELAY 0x10 +#define SOCK_TOS_MAXTP 0x08 +#define SOCK_TOS_MAXREL 0x04 +#define SOCK_TOS_MINCOST 0x02 + +struct dgrambuf +{ + struct dgrambuf *next; + struct sockaddr *addr; + socklen_t addrlen; + void *data; + size_t size; +}; + +struct socket +{ + struct socket *next, *prev; + int refcount; + int fd; + int isrealsocket; /* Bleh... */ + int family; + int tos; + int type; + int state; + int ignread; + int events; + int close; + struct sockaddr *remote; + socklen_t remotelen; + union + { + struct + { + struct dgrambuf *f, *l; + } d; + struct + { + void *buf; + size_t bufsize; + size_t datasize; + } s; + } outbuf; + union + { + struct + { + struct dgrambuf *f, *l; + } d; + struct + { + void *buf; + size_t bufsize; + size_t datasize; + } s; + } inbuf; + void (*conncb)(struct socket *sk, int err, void *data); + void (*errcb)(struct socket *sk, int err, void *data); + void (*readcb)(struct socket *sk, void *data); + void (*writecb)(struct socket *sk, void *data); + void (*acceptcb)(struct socket *sk, struct socket *newsk, void *data); + void *data; +}; + +void putsock(struct socket *sk); +void getsock(struct socket *sk); +struct socket *netcslisten(int type, struct sockaddr *name, socklen_t namelen, void (*func)(struct socket *, struct socket *, void *), void *data); +struct socket *netcslistenlocal(int type, struct sockaddr *name, socklen_t namelen, void (*func)(struct socket *, struct socket *, void *), void *data); +struct socket *netcsconn(struct sockaddr *addr, socklen_t addrlen, void (*func)(struct socket *, int, void *), void *data); +int pollsocks(int timeout); +void sockqueue(struct socket *sk, void *data, size_t size); +size_t sockqueuesize(struct socket *sk); +int netresolve(char *addr, void (*callback)(struct sockaddr *addr, int addrlen, void *data), void *data); +struct socket *netcsdgram(struct sockaddr *name, socklen_t namelen); +struct socket *netdupsock(struct socket *sk); +void netdgramconn(struct socket *sk, struct sockaddr *addr, socklen_t addrlen); +int sockgetlocalname(struct socket *sk, struct sockaddr **namebuf, socklen_t *lenbuf); +int sockgetremotename(struct socket *sk, struct sockaddr **namebuf, socklen_t *lenbuf); +void closesock(struct socket *sk); +void *sockgetinbuf(struct socket *sk, size_t *size); +struct socket *wrapsock(int fd); +size_t sockgetdatalen(struct socket *sk); +int getpublicaddr(int af, struct sockaddr **addr, socklen_t *lenbuf); +int socksettos(struct socket *sk, int tos); +char *formataddress(struct sockaddr *arg, socklen_t arglen); + +#endif diff --git a/daemon/search.c b/daemon/search.c new file mode 100644 index 0000000..040ec30 --- /dev/null +++ b/daemon/search.c @@ -0,0 +1,1183 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "utils.h" +#include "log.h" +#include "sysevents.h" +#include "filenet.h" +#include "search.h" + +#define TOK_STR 0 +#define TOK_SE 1 +#define TOK_OP 2 +#define TOK_CP 3 + +struct srchlist +{ + struct srchlist *next, *prev; + struct search *srch; +}; + +struct tok +{ + struct tok *next; + int type; + union + { + wchar_t *str; + struct sexpr *se; + } d; +}; + +struct reinfo +{ + wchar_t *begstr, *endstr, *onestr; + struct wcslist *strs; +}; + +struct bound +{ + int min, max; +}; + +static void trycommit(void); + +struct search *searches = NULL; +static struct srchlist *searchqueue = NULL; +static struct timer *committimer = NULL; +GCBCHAIN(newsrchcb, struct search *); + +wchar_t *regexunquotesimple(wchar_t *re) +{ + wchar_t *specials, *buf, *p; + + specials = L"\\^$.*+?[{()|"; + buf = smalloc((wcslen(re) + 1) * sizeof(wchar_t)); + p = buf; + for(; *re != L'\0'; re++) + { + if(*re == L'\\') + { + re++; + if(!*re) + { + *p = L'\0'; + return(buf); + } + *(p++) = *re; + } else { + if(wcschr(specials, *re) != NULL) + { + free(buf); + return(NULL); + } + *(p++) = *re; + } + } + *p = L'\0'; + return(buf); +} + +static void freesln(struct wcslist *ln, struct wcslist **list) +{ + if(ln->prev != NULL) + ln->prev->next = ln->next; + if(ln->next != NULL) + ln->next->prev = ln->prev; + if(ln == *list) + *list = ln->next; + free(ln->str); + free(ln); +} + +void freesl(struct wcslist **list) +{ + while(*list != NULL) + freesln(*list, list); +} + +static struct wcslist *newsl(struct wcslist **list, wchar_t *str) +{ + struct wcslist *ln; + + ln = smalloc(sizeof(*ln)); + memset(ln, 0, sizeof(*ln)); + ln->str = swcsdup(str); + ln->len = wcslen(str); + ln->next = *list; + ln->prev = NULL; + if(*list != NULL) + (*list)->prev = ln; + *list = ln; + return(ln); +} + +static void slmerge1(struct wcslist **list, wchar_t *str) +{ + size_t len; + struct wcslist *cur, *next; + + len = wcslen(str); + for(cur = *list; cur != NULL; cur = next) + { + next = cur->next; + if(len <= cur->len) + { + if(((len < cur->len) && wcsexists(cur->str, str)) || !wcscmp(str, cur->str)) + return; + } else if(len > cur->len) { + if(wcsexists(str, cur->str)) + freesln(cur, list); + } + } + newsl(list, str); +} + +void slmergemax(struct wcslist **dest, struct wcslist *src) +{ + for(; src != NULL; src = src->next) + slmerge1(dest, src->str); +} + +static struct wcslist *makeminlist1(wchar_t *s1, wchar_t *s2) +{ + int i; + wchar_t *p1, *p2, c; + struct wcslist *list; + + list = NULL; + for(p1 = s1; *p1 != L'\0'; p1++) + { + for(p2 = s2; *p2 != L'\0'; p2++) + { + for(i = 0; (p1[i] != L'\0') && (p2[i] != L'\0') && (towlower(p1[i]) == towlower(p2[i])); i++); + if(i > 0) + { + c = p2[i]; + p2[i] = L'\0'; + slmerge1(&list, p2); + p2[i] = c; + } + } + } + return(list); +} + +struct wcslist *slmergemin(struct wcslist *l1, struct wcslist *l2) +{ + struct wcslist *cur1, *cur2, *list, *rlist; + + list = NULL; + for(cur1 = l1; cur1 != NULL; cur1 = cur1->next) + { + for(cur2 = l2; cur2 != NULL; cur2 = cur2->next) + { + rlist = makeminlist1(cur1->str, cur2->str); + slmergemax(&list, rlist); + freesl(&rlist); + } + } + return(list); +} + +static struct bound readbound(wchar_t *p, wchar_t **endret) +{ + struct bound ret; + + switch(*p) + { + case L'?': + ret.min = 0; + ret.max = 1; + p++; + break; + case L'*': + ret.min = 0; + ret.max = -1; + p++; + break; + case L'+': + ret.min = 1; + ret.max = -1; + p++; + break; + case L'{': + p++; + ret.min = wcstol(p, &p, 10); + if(*p == L',') + { + p++; + if(*p == L'}') + ret.max = -1; + else + ret.max = wcstol(p, &p, 10); + } else { + ret.max = ret.min; + } + if(*p != L'}') + { + /* Cannot happen in a validated regex... */ + flog(LOG_WARNING, "BUG? \"Impossible\" case encountered in search.c:readbound() (No `}' after `{'-type bound!"); + } else { + p++; + } + break; + default: + ret.min = 1; + ret.max = 1; + break; + } + if(endret != NULL) + *endret = p; + return(ret); +} + +#define fnc(p) do {if((p) != NULL) free(p); (p) = NULL; p ## size = 0; p ## data = 0;} while(0) + +static struct reinfo analyzere(wchar_t *re, wchar_t **endret, wchar_t endc) +{ + int i, commit, parsealt; + struct reinfo ret, sinf; + struct bound b; + wchar_t *cs, *ns; + size_t cssize, csdata, nssize, nsdata, len1, len2, maxlen, minlen; + wchar_t beg, c; + struct wcslist *list; + + memset(&ret, 0, sizeof(ret)); + commit = parsealt = 0; + beg = 1; + cs = ns = NULL; + cssize = csdata = nssize = nsdata = 0; + while((*re != endc) && !parsealt) + { + switch(*re) + { + case L'$': + case L'^': + re++; + commit = 1; + break; + case L'.': + re++; + b = readbound(re, &re); + if(b.max != 0) + commit = 1; + break; + case L'(': + re++; + sinf = analyzere(re, &re, L')'); + re++; + b = readbound(re, &re); + if(sinf.onestr != NULL) + { + for(i = 0; i < b.min; i++) + { + bufcat(cs, sinf.onestr, wcslen(sinf.onestr)); + bufcat(ns, sinf.onestr, wcslen(sinf.onestr)); + } + if((b.max == -1) || (b.max > b.min)) + commit = 1; + } else { + commit = 1; + if(b.min > 0) + { + if(sinf.begstr != NULL) + bufcat(cs, sinf.begstr, wcslen(sinf.begstr)); + if(sinf.endstr != NULL) + bufcat(ns, sinf.endstr, wcslen(sinf.endstr)); + } + } + if(sinf.begstr != NULL) + free(sinf.begstr); + if(sinf.endstr != NULL) + free(sinf.endstr); + if(sinf.onestr != NULL) + free(sinf.onestr); + if(b.min > 0) + slmergemax(&ret.strs, sinf.strs); + freesl(&sinf.strs); + break; + case L'[': + c = L'\0'; + re += 2; + /* Must exist in a validated RE... */ + while(*(re++) != L']'); + readbound(re, &re); + commit = 1; + break; + case L'|': + re++; + parsealt = 1; + break; + case L'\\': + re++; + /* A validated RE cannot end in a backslash, so fall + * through */ + default: + c = *(re++); + b = readbound(re, &re); + for(i = 0; i < b.min; i++) + addtobuf(cs, c); + if((b.max == -1) || (b.max > b.min)) + commit = 1; + break; + } + if(commit) + { + if(cs != NULL) + addtobuf(cs, L'\0'); + if(beg) + { + if(cs != NULL) + ret.begstr = swcsdup(cs); + beg = 0; + } + if(cs != NULL) + slmerge1(&ret.strs, cs); + fnc(cs); + commit = 0; + if(ns != NULL) + { + cs = swcsdup(ns); + cssize = nssize; + csdata = nsdata; + fnc(ns); + } + } + } + if(cs != NULL) + { + addtobuf(cs, L'\0'); + if(beg) + ret.onestr = swcsdup(cs); + else + ret.endstr = swcsdup(cs); + slmerge1(&ret.strs, cs); + fnc(cs); + } + if(parsealt) + { + sinf = analyzere(re, &re, endc); + list = slmergemin(ret.strs, sinf.strs); + freesl(&ret.strs); + freesl(&sinf.strs); + ret.strs = list; + if(sinf.begstr != NULL) + { + if(ret.begstr != NULL) + { + for(i = 0; (sinf.begstr[i] != L'\0') && (ret.begstr != L'\0') && (ret.begstr[i] == sinf.begstr[i]); i++); + if(i == 0) + free(ret.begstr); + else + ret.begstr[i] = L'\0'; + } + free(sinf.begstr); + } else { + if(ret.begstr != NULL) + { + free(ret.begstr); + ret.begstr = NULL; + } + } + if(sinf.endstr != NULL) + { + if(ret.endstr != NULL) + { + len1 = wcslen(ret.endstr); + len2 = wcslen(sinf.endstr); + if(len1 < len2) + { + minlen = len1; + maxlen = len2; + } else { + minlen = len2; + maxlen = len1; + } + for(i = 1; (i <= minlen) && (ret.endstr[len1 - i] == sinf.endstr[len2 - i]); i++); + if(i == 1) + free(ret.endstr); + else if(i <= maxlen) + wmemmove(ret.endstr, ret.endstr + (len1 - i) + 1, i); + } + free(sinf.endstr); + } else { + if(ret.endstr != NULL) + { + free(ret.endstr); + ret.endstr = NULL; + } + } + if(sinf.onestr != NULL) + { + if(ret.onestr != NULL) + { + /* XXX: Comparing beginning and end of the onestrs and + * create begstr and endstr if there isn't an exact + * match.*/ + if(wcscmp(ret.onestr, sinf.onestr)) + { + free(ret.onestr); + ret.onestr = NULL; + } + } + free(sinf.onestr); + } else { + if(ret.onestr != NULL) + { + free(ret.onestr); + ret.onestr = NULL; + } + } + } + if(endret != NULL) + *endret = re; + return(ret); +} + +#undef fnc + +struct wcslist *regexfindstrings(wchar_t *re) +{ + struct reinfo i; + + i = analyzere(re, NULL, L'\0'); + if(i.begstr != NULL) + free(i.begstr); + if(i.endstr != NULL) + free(i.endstr); + if(i.onestr != NULL) + free(i.onestr); + return(i.strs); +} + +static struct sexpr *newsexpr(void) +{ + struct sexpr *sexpr; + + sexpr = smalloc(sizeof(*sexpr)); + memset(sexpr, 0, sizeof(*sexpr)); + sexpr->refcount = 1; + return(sexpr); +} + +void getsexpr(struct sexpr *sexpr) +{ + sexpr->refcount++; +} + +void putsexpr(struct sexpr *sexpr) +{ + if(--sexpr->refcount != 0) + return; + if(sexpr->l != NULL) + putsexpr(sexpr->l); + if(sexpr->r != NULL) + putsexpr(sexpr->r); + if((sexpr->op == SOP_NAMERE) || (sexpr->op == SOP_LINKRE)) + { + if(sexpr->d.re.sre != NULL) + free(sexpr->d.re.sre); + if(sexpr->d.re.inited) + regfree(&sexpr->d.re.cre); + } + if((sexpr->op == SOP_NAMESS) || (sexpr->op == SOP_LINKSS)) + { + if(sexpr->d.s != NULL) + free(sexpr->d.s); + } + free(sexpr); +} + +static struct tok *newtok(void) +{ + struct tok *tok; + + tok = smalloc(sizeof(*tok)); + memset(tok, 0, sizeof(*tok)); + tok->next = NULL; + return(tok); +} + +static void freetok(struct tok *tok) +{ + if((tok->type == TOK_STR) && (tok->d.str != NULL)) + free(tok->d.str); + if((tok->type == TOK_SE) && (tok->d.se != NULL)) + putsexpr(tok->d.se); + free(tok); +} + +static void pushtok(struct tok *tok, struct tok **st) +{ + tok->next = *st; + *st = tok; +} + +static struct tok *poptok(struct tok **st) +{ + struct tok *tok; + + tok = *st; + *st = (*st)->next; + return(tok); +} + +int calccost(struct sexpr *sexpr) +{ + sexpr->tcost = sexpr->cost; + if(sexpr->l != NULL) + sexpr->tcost += calccost(sexpr->l); + if(sexpr->r != NULL) + sexpr->tcost += calccost(sexpr->r); + return(sexpr->tcost); +} + +struct sexpr *parsesexpr(int argc, wchar_t **argv) +{ + int i, done, std; + struct tok *st, *tok, *tok2; + struct sexpr *sexpr; + char *buf; + wchar_t *wbuf; + + std = 0; + st = NULL; + for(i = 0; i < argc; i++) + { + pushtok(tok = newtok(), &st); + tok->type = TOK_STR; + tok->d.str = swcsdup(argv[i]); + std++; + do + { + done = 1; + if((st->type == TOK_STR) && !wcscmp(st->d.str, L"(")) + { + freetok(poptok(&st)); + pushtok(tok = newtok(), &st); + tok->type = TOK_OP; + done = 0; + } else if((st->type == TOK_STR) && !wcscmp(st->d.str, L")")) { + freetok(poptok(&st)); + pushtok(tok = newtok(), &st); + tok->type = TOK_CP; + done = 0; + } else if((st->type == TOK_STR) && (!wcsncmp(st->d.str, L"N~", 2) || !wcsncmp(st->d.str, L"L~", 2))) { + tok2 = poptok(&st); + pushtok(tok = newtok(), &st); + tok->type = TOK_SE; + sexpr = newsexpr(); + if((wbuf = regexunquotesimple(tok2->d.str + 2)) != NULL) + { + if(tok2->d.str[0] == L'N') + sexpr->op = SOP_NAMESS; + else + sexpr->op = SOP_LINKSS; + sexpr->d.s = wbuf; + sexpr->cost = 5; + } else { + if(tok2->d.str[0] == L'N') + sexpr->op = SOP_NAMERE; + else + sexpr->op = SOP_LINKRE; + sexpr->cost = 20; + if((buf = icwcstombs(tok2->d.str + 2, "UTF-8")) == NULL) + { + freetok(tok2); + putsexpr(sexpr); + goto out_err; + } + if(regcomp(&sexpr->d.re.cre, buf, REG_EXTENDED | REG_ICASE | REG_NOSUB)) + { + freetok(tok2); + free(buf); + putsexpr(sexpr); + goto out_err; + } + free(buf); + sexpr->d.re.inited = 1; + sexpr->d.re.sre = swcsdup(tok2->d.str + 2); + } + getsexpr(tok->d.se = sexpr); + freetok(tok2); + putsexpr(sexpr); + done = 0; + } else if((st->type == TOK_STR) && (!wcsncmp(st->d.str, L"S<", 2) || !wcsncmp(st->d.str, L"S=", 2) || !wcsncmp(st->d.str, L"S>", 2))) { + tok2 = poptok(&st); + pushtok(tok = newtok(), &st); + tok->type = TOK_SE; + sexpr = newsexpr(); + if(tok2->d.str[1] == L'<') + sexpr->op = SOP_SIZELT; + else if(tok2->d.str[1] == L'=') + sexpr->op = SOP_SIZEEQ; + else + sexpr->op = SOP_SIZEGT; + sexpr->d.n = wcstol(tok2->d.str + 2, NULL, 0); + sexpr->cost = 0; + getsexpr(tok->d.se = sexpr); + freetok(tok2); + putsexpr(sexpr); + done = 0; + } else if((std >= 3) && (st->type == TOK_CP) && (st->next->type == TOK_SE) && (st->next->next->type == TOK_OP)) { + freetok(poptok(&st)); + tok = poptok(&st); + freetok(poptok(&st)); + pushtok(tok, &st); + std -= 2; + done = 0; + } else if((std >= 2) && (st->type == TOK_SE) && (st->next->type == TOK_STR) && !wcscmp(st->next->d.str, L"!")) { + sexpr = newsexpr(); + sexpr->op = SOP_NOT; + sexpr->cost = 0; + getsexpr(sexpr->l = st->d.se); + freetok(poptok(&st)); + freetok(poptok(&st)); + pushtok(tok = newtok(), &st); + tok->type = TOK_SE; + getsexpr(tok->d.se = sexpr); + putsexpr(sexpr); + std -= 1; + done = 0; + } else if((std >= 3) && (st->type == TOK_SE) && (st->next->type == TOK_STR) && (!wcscmp(st->next->d.str, L"&") || !wcscmp(st->next->d.str, L"|")) && (st->next->next->type == TOK_SE)) { + sexpr = newsexpr(); + if(!wcscmp(st->next->d.str, L"&")) + sexpr->op = SOP_AND; + else + sexpr->op = SOP_OR; + sexpr->cost = 0; + getsexpr(sexpr->l = st->next->next->d.se); + getsexpr(sexpr->r = st->d.se); + freetok(poptok(&st)); + freetok(poptok(&st)); + freetok(poptok(&st)); + pushtok(tok = newtok(), &st); + tok->type = TOK_SE; + getsexpr(tok->d.se = sexpr); + putsexpr(sexpr); + std -= 2; + done = 0; + } + } while(!done); + } + if((st == NULL) || (st->next != NULL) || (st->type != TOK_SE)) + goto out_err; + getsexpr(sexpr = st->d.se); + freetok(st); + calccost(sexpr); + return(sexpr); + + out_err: + while(st != NULL) + freetok(poptok(&st)); + return(NULL); +} + +void optsexpr(struct sexpr *sexpr) +{ + struct sexpr *buf; + + if((sexpr->l != NULL) && (sexpr->r != NULL)) + { + if(sexpr->l->tcost > sexpr->r->tcost) + { + buf = sexpr->r; + sexpr->r = sexpr->l; + sexpr->l = buf; + } + } + if(sexpr->l != NULL) + optsexpr(sexpr->l); + if(sexpr->r != NULL) + optsexpr(sexpr->r); +} + +struct wcslist *findsexprstrs(struct sexpr *sexpr) +{ + struct wcslist *list, *l1, *l2; + + list = NULL; + switch(sexpr->op) + { + case SOP_AND: + list = findsexprstrs(sexpr->l); + l1 = findsexprstrs(sexpr->r); + slmergemax(&list, l1); + freesl(&l1); + break; + case SOP_OR: + l1 = findsexprstrs(sexpr->l); + l2 = findsexprstrs(sexpr->r); + list = slmergemin(l1, l2); + freesl(&l1); + freesl(&l2); + break; + case SOP_NOT: + break; + case SOP_NAMERE: + case SOP_LINKRE: + list = regexfindstrings(sexpr->d.re.sre); + break; + case SOP_NAMESS: + case SOP_LINKSS: + slmerge1(&list, sexpr->d.s); + break; + default: + break; + } + return(list); +} + +static void unlinksqueue(struct srchlist *ln) +{ + if(ln->prev != NULL) + ln->prev->next = ln->next; + if(ln->next != NULL) + ln->next->prev = ln->prev; + if(ln == searchqueue) + searchqueue = ln->next; + free(ln); +} + +static void ctexpire(int cancelled, void *data) +{ + committimer = NULL; + if(!cancelled) + trycommit(); +} + +static void estimatequeue(void) +{ + struct srchlist *cur; + struct srchfnnlist *ln; + time_t now, start; + + if(searchqueue == NULL) + return; + start = now = time(NULL); + for(ln = searchqueue->srch->fnl; ln != NULL; ln = ln->next) + { + if((ln->fn->lastsrch != 0) && (ln->fn->lastsrch + ln->fn->srchwait > start)) + start = ln->fn->lastsrch + ln->fn->srchwait; + } + if(start != searchqueue->srch->eta) + { + searchqueue->srch->eta = start; + CBCHAINDOCB(searchqueue->srch, search_eta, searchqueue->srch); + } + for(cur = searchqueue->next; cur != NULL; cur = cur->next) + { + now = start; + for(ln = cur->srch->fnl; ln != NULL; ln = ln->next) + { + if(now + ln->fn->srchwait > start) + start = now + ln->fn->srchwait; + } + if(start != cur->srch->eta) + { + cur->srch->eta = start; + CBCHAINDOCB(cur->srch, search_eta, cur->srch); + } + } + if((committimer == NULL) || ((time_t)committimer->at != searchqueue->srch->eta)) + { + if(committimer != NULL) + canceltimer(committimer); + committimer = timercallback(searchqueue->srch->eta, ctexpire, NULL); + } +} + +struct search *findsearch(int id) +{ + struct search *srch; + + for(srch = searches; srch != NULL; srch = srch->next) + { + if(srch->id == id) + break; + } + return(srch); +} + +struct search *newsearch(wchar_t *owner, struct sexpr *sexpr) +{ + struct search *srch; + static int id = 0; + + srch = smalloc(sizeof(*srch)); + memset(srch, 0, sizeof(*srch)); + srch->id = id++; + srch->owner = swcsdup(owner); + if((srch->sexpr = sexpr) != NULL) + getsexpr(srch->sexpr); + CBCHAININIT(srch, search_eta); + CBCHAININIT(srch, search_commit); + CBCHAININIT(srch, search_result); + CBCHAININIT(srch, search_destroy); + srch->next = searches; + srch->prev = NULL; + if(searches != NULL) + searches->prev = srch; + searches = srch; + return(srch); +} + +static void srchexpire(int cancelled, struct search *srch) +{ + srch->freetimer = NULL; + if(!cancelled) + freesearch(srch); +} + +static void trycommit(void) +{ + struct srchfnnlist *ln; + struct search *srch; + time_t now; + + if(searchqueue == NULL) + return; + srch = searchqueue->srch; + now = time(NULL); + for(ln = srch->fnl; ln != NULL; ln = ln->next) + { + if(now < ln->fn->lastsrch + ln->fn->srchwait) + break; + } + if(ln != NULL) + return; + unlinksqueue(searchqueue); + srch->state = SRCH_RUN; + srch->eta = time(NULL); + srch->committime = ntime(); + srch->freetimer = timercallback(ntime() + 300, (void (*)(int, void *))srchexpire, srch); + CBCHAINDOCB(srch, search_commit, srch); + for(ln = srch->fnl; ln != NULL; ln = ln->next) + fnetsearch(ln->fn, srch, ln); + estimatequeue(); +} + +void freesearch(struct search *srch) +{ + struct srchfnnlist *ln; + struct srchlist *sln; + + if(srch->prev != NULL) + srch->prev->next = srch->next; + if(srch->next != NULL) + srch->next->prev = srch->prev; + if(srch == searches) + searches = srch->next; + estimatequeue(); + if(srch->freetimer != NULL) + canceltimer(srch->freetimer); + CBCHAINDOCB(srch, search_destroy, srch); + CBCHAINFREE(srch, search_eta); + CBCHAINFREE(srch, search_commit); + CBCHAINFREE(srch, search_result); + CBCHAINFREE(srch, search_destroy); + while(srch->results != NULL) + freesrchres(srch->results); + for(sln = searchqueue; sln != NULL; sln = sln->next) + { + if(sln->srch == srch) + { + unlinksqueue(sln); + break; + } + } + while(srch->fnl != NULL) + { + ln = srch->fnl; + srch->fnl = ln->next; + CBCHAINDOCB(ln, searchfnl_destroy, ln); + CBCHAINFREE(ln, searchfnl_destroy); + putfnetnode(ln->fn); + free(ln); + } + if(srch->sexpr != NULL) + putsexpr(srch->sexpr); + if(srch->owner != NULL) + free(srch->owner); + free(srch); +} + +void searchaddfn(struct search *srch, struct fnetnode *fn) +{ + struct srchfnnlist *ln; + + for(ln = srch->fnl; ln != NULL; ln = ln->next) + { + if(ln->fn == fn) + return; + } + ln = smalloc(sizeof(*ln)); + memset(ln, 0, sizeof(*ln)); + getfnetnode(ln->fn = fn); + CBCHAININIT(ln, searchfnl_destroy); + ln->next = srch->fnl; + srch->fnl = ln; +} + +static void linksearch(struct search *srch, struct srchlist *prev) +{ + struct srchlist *new; + + new = smalloc(sizeof(*new)); + new->srch = srch; + if(prev == NULL) + { + new->prev = NULL; + new->next = searchqueue; + if(searchqueue != NULL) + searchqueue->prev = new; + searchqueue = new; + } else { + new->prev = prev; + if((new->next = prev->next) != NULL) + new->next->prev = new; + prev->next = new; + } + GCBCHAINDOCB(newsrchcb, srch); + estimatequeue(); +} + +/* + * queuesearch is also the "scheduler" function - it finds a suitable + * place in the queue for the new search. I'll make a weak attempt at + * describing the algorithm: + * First, we find the first search that doesn't have a lower priority + * than this one. If there is no such, we just link this one onto the + * end of the queue. + * Then, if we have a search of this priority in the queue with the + * same owner as the new search, we set lastmine to the search after + * that one, otherwise, lastmine is the first search of this + * priority. If lastmine is discovered either to not exist (that is, + * our last search is at the end of the queue), or to be of lower + * priority (higher number), we link it in at the appropriate end. + * Then, we find the next search of the same priority and owner as + * lastmine, and link this search in before it. That should yield a + * 'round-robin-like' scheduling within priority boundaries. I think. + */ +void queuesearch(struct search *srch) +{ + struct srchlist *cur, *lastmine, *prev; + wchar_t *nexto; + + for(prev = NULL, cur = searchqueue; cur != NULL; prev = cur, cur = cur->next) + { + if(cur->srch->prio >= srch->prio) + break; + } + if(cur == NULL) + { + linksearch(srch, prev); + return; + } + lastmine = cur; + for(; cur != NULL; prev = cur, cur = cur->next) + { + if(!wcscmp(cur->srch->owner, srch->owner)) + lastmine = cur->next; + if(cur->srch->prio > srch->prio) + break; + } + if((lastmine == NULL) || (lastmine->srch->prio > srch->prio)) + { + linksearch(srch, prev); + return; + } + nexto = lastmine->srch->owner; + for(cur = lastmine->next; cur != NULL; prev = cur, cur = cur->next) + { + if(!wcscmp(cur->srch->owner, nexto)) + break; + if(cur->srch->prio > srch->prio) + break; + } + if(cur == NULL) + { + linksearch(srch, prev); + return; + } + linksearch(srch, prev); +} + +static int srisvalid(struct srchres *sr, struct sexpr *sexpr) +{ + int ret; + char *buf; + wchar_t *p; + + switch(sexpr->op) + { + case SOP_FALSE: + return(0); + case SOP_TRUE: + return(1); + case SOP_AND: + if(!srisvalid(sr, sexpr->l)) + return(0); + return(srisvalid(sr, sexpr->r)); + case SOP_OR: + if(srisvalid(sr, sexpr->l)) + return(1); + return(srisvalid(sr, sexpr->r)); + case SOP_NOT: + return(!srisvalid(sr, sexpr->l)); + case SOP_NAMERE: + if((buf = icwcstombs(sr->filename, "UTF-8")) == NULL) + return(0); + ret = regexec(&sexpr->d.re.cre, buf, 0, NULL, 0); + free(buf); + return(!ret); + case SOP_LINKRE: + p = sr->filename; + if(sr->fnet->filebasename != NULL) + p = sr->fnet->filebasename(p); + if((buf = icwcstombs(p, "UTF-8")) == NULL) + return(0); + ret = regexec(&sexpr->d.re.cre, buf, 0, NULL, 0); + free(buf); + return(!ret); + case SOP_NAMESS: + return(wcsexists(sr->filename, sexpr->d.s)); + case SOP_LINKSS: + p = sr->filename; + if(sr->fnet->filebasename != NULL) + p = sr->fnet->filebasename(p); + return(wcsexists(p, sexpr->d.s)); + case SOP_SIZELT: + return(sr->size < sexpr->d.n); + case SOP_SIZEEQ: + return(sr->size == sexpr->d.n); + case SOP_SIZEGT: + return(sr->size > sexpr->d.n); + } + return(0); +} + +struct srchres *newsrchres(struct fnet *fnet, wchar_t *filename, wchar_t *peerid) +{ + struct srchres *sr; + + sr = smalloc(sizeof(*sr)); + memset(sr, 0, sizeof(*sr)); + sr->size = -1; + sr->slots = -1; + sr->fnet = fnet; + sr->filename = swcsdup(filename); + sr->peerid = swcsdup(peerid); + return(sr); +} + +void freesrchres(struct srchres *sr) +{ + if(sr->next != NULL) + sr->next->prev = sr->prev; + if(sr->prev != NULL) + sr->prev->next = sr->next; + if(sr->srch != NULL) + { + if(sr->srch->results == sr) + sr->srch->results = sr->next; + sr->srch->numres--; + } + if(sr->filename != NULL) + free(sr->filename); + if(sr->peerid != NULL) + free(sr->peerid); + if(sr->peernick != NULL) + free(sr->peernick); + if(sr->fn != NULL) + putfnetnode(sr->fn); + free(sr); +} + +struct srchres *dupsrchres(struct srchres *sr) +{ + struct srchres *new; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + new->size = sr->size; + new->slots = sr->slots; + new->fnet = sr->fnet; + if(sr->peerid != NULL) + new->peerid = swcsdup(sr->peerid); + if(sr->peernick != NULL) + new->peernick = swcsdup(sr->peernick); + if(sr->filename != NULL) + new->filename = swcsdup(sr->filename); + if(sr->fn != NULL) + getfnetnode(new->fn = sr->fn); + return(new); +} + +static void linksr(struct search *srch, struct srchres *sr) +{ + sr->prev = NULL; + sr->next = srch->results; + if(srch->results != NULL) + srch->results->prev = sr; + srch->results = sr; + sr->srch = srch; + sr->time = ntime() - srch->committime; + srch->numres++; +} + +void submitsrchres(struct srchres *sr) +{ + struct search *srch; + struct srchfnnlist *ln; + struct srchres *dsr; + + for(srch = searches; srch != NULL; srch = srch->next) + { + if(srch->state == SRCH_RUN) + { + if(!srisvalid(sr, srch->sexpr)) + continue; + for(ln = srch->fnl; ln != NULL; ln = ln->next) + { + if(((sr->fn != NULL) && (ln->fn == sr->fn)) || ((sr->fn == NULL) && (sr->fnet == ln->fn->fnet))) + break; + } + if(ln != NULL) + { + dsr = dupsrchres(sr); + linksr(srch, dsr); + CBCHAINDOCB(srch, search_result, srch, dsr); + } + } + } +} diff --git a/daemon/search.h b/daemon/search.h new file mode 100644 index 0000000..dff4c80 --- /dev/null +++ b/daemon/search.h @@ -0,0 +1,132 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _SEARCH_H +#define _SEARCH_H + +#include "filenet.h" +#include "sysevents.h" +#include +#include + +#define SOP_FALSE 0 +#define SOP_TRUE 1 +#define SOP_AND 2 +#define SOP_OR 3 +#define SOP_NOT 4 +#define SOP_NAMERE 5 +#define SOP_NAMESS 6 +#define SOP_LINKRE 7 +#define SOP_LINKSS 8 +#define SOP_SIZEGT 9 +#define SOP_SIZELT 10 +#define SOP_SIZEEQ 11 + +#define SRCH_WAIT 0 +#define SRCH_RUN 1 + +struct wcslist +{ + struct wcslist *next, *prev; + wchar_t *str; + size_t len; +}; + +struct sexpr +{ + int refcount; + int op; + struct sexpr *l, *r; + int cost, tcost; + union + { + struct + { + wchar_t *sre; + regex_t cre; + int inited; + } re; + wchar_t *s; + int n; + } d; +}; + +struct srchfnnlist +{ + struct srchfnnlist *next; + struct fnetnode *fn; + void *fnetdata; + CBCHAIN(searchfnl_destroy, struct srchfnnlist *ln); +}; + +struct search +{ + struct search *next, *prev; + int id; + int state; + wchar_t *owner; + int prio; + time_t eta; + double committime; + struct sexpr *sexpr; + struct srchfnnlist *fnl; + struct srchres *results; + int numres; + struct timer *freetimer; + CBCHAIN(search_eta, struct search *srch); + CBCHAIN(search_commit, struct search *srch); + CBCHAIN(search_result, struct search *srch, struct srchres *sr); + CBCHAIN(search_destroy, struct search *srch); +}; + +struct srchres +{ + struct srchres *next, *prev; + struct search *srch; + wchar_t *filename; + struct fnet *fnet; + wchar_t *peerid, *peernick; + size_t size; + int slots; + struct fnetnode *fn; + double time; +}; + +wchar_t *regexunquotesimple(wchar_t *re); +struct sexpr *parsesexpr(int argc, wchar_t **argv); +void optsexpr(struct sexpr *sexpr); +void getsexpr(struct sexpr *sexpr); +void putsexpr(struct sexpr *sexpr); +struct search *newsearch(wchar_t *owner, struct sexpr *sexpr); +void searchaddfn(struct search *srch, struct fnetnode *fn); +void queuesearch(struct search *srch); +void freesearch(struct search *srch); +struct wcslist *regexfindstrings(wchar_t *re); +void freesl(struct wcslist **list); +void slmergemax(struct wcslist **dest, struct wcslist *src); +struct wcslist *slmergemin(struct wcslist *l1, struct wcslist *l2); +struct wcslist *findsexprstrs(struct sexpr *sexpr); +struct srchres *newsrchres(struct fnet *fnet, wchar_t *filename, wchar_t *peerid); +void freesrchres(struct srchres *sr); +void submitsrchres(struct srchres *sr); +struct search *findsearch(int id); + +extern struct search *searches; +EGCBCHAIN(newsrchcb, struct search *); + +#endif diff --git a/daemon/sysevents.h b/daemon/sysevents.h new file mode 100644 index 0000000..47e574f --- /dev/null +++ b/daemon/sysevents.h @@ -0,0 +1,52 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _SYSEVENTS_H +#define _SYSEVENTS_H + +#include +#include "auth.h" + +#define FD_END -1 +#define FD_PIPE 0 +#define FD_FILE 1 + +struct timer +{ + struct timer *next, *prev; + double at; + void (*func)(int cancelled, void *data); + void *data; +}; + +struct child +{ + struct child *next, *prev; + pid_t pid; + void (*callback)(pid_t pid, int status, void *data); + void *data; + int status; + volatile int finished; +}; + +void childcallback(pid_t pid, void (*func)(pid_t, int, void *), void *data); +struct timer *timercallback(double at, void (*func)(int, void *), void *data); +void canceltimer(struct timer *timer); +pid_t forksess(uid_t user, struct authhandle *auth, void (*ccbfunc)(pid_t, int, void *), void *data, ...); + +#endif diff --git a/daemon/tiger.c b/daemon/tiger.c new file mode 100644 index 0000000..6465dbb --- /dev/null +++ b/daemon/tiger.c @@ -0,0 +1,748 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "tiger.h" +#include "utils.h" + +/* + * This won't be a fast implementation of Tiger. For now, I just want + * to be done with it and have it somewhat portable. + */ + +static unsigned long long table[]; + +void inittiger(struct tigerhash *th) +{ + th->a = 0x0123456789abcdefULL; + th->b = 0xfedcba9876543210ULL; + th->c = 0xf096a5b4c3b2e187ULL; + th->offset = 0; + th->len = 0; +} + +static void round(unsigned long long *a, unsigned long long *b, unsigned long long *c, unsigned long long x, int mul) +{ + *c ^= x; + *a -= table[((*c >> 0) & 0xff) + 0x0000] ^ table[((*c >> 16) & 0xff) + 0x0100] ^ table[((*c >> 32) & 0xff) + 0x0200] ^ table[((*c >> 48) & 0xff) + 0x0300]; + *b += table[((*c >> 8) & 0xff) + 0x0300] ^ table[((*c >> 24) & 0xff) + 0x0200] ^ table[((*c >> 40) & 0xff) + 0x0100] ^ table[((*c >> 56) & 0xff) + 0x0000]; + *b *= mul; +} + +static void pass(unsigned long long *a, unsigned long long *b, unsigned long long *c, unsigned long long *x, int mul) +{ + round(a, b, c, x[0], mul); + round(b, c, a, x[1], mul); + round(c, a, b, x[2], mul); + round(a, b, c, x[3], mul); + round(b, c, a, x[4], mul); + round(c, a, b, x[5], mul); + round(a, b, c, x[6], mul); + round(b, c, a, x[7], mul); +} + +static void key_schedule(unsigned long long *x) +{ + x[0] -= x[7] ^ 0xa5a5a5a5a5a5a5a5ULL; + x[1] ^= x[0]; + x[2] += x[1]; + x[3] -= x[2] ^ ((~x[1]) << 19); + x[4] ^= x[3]; + x[5] += x[4]; + x[6] -= x[5] ^ ((~x[4]) >> 23); + x[7] ^= x[6]; + x[0] += x[7]; + x[1] -= x[0] ^ ((~x[7]) << 19); + x[2] ^= x[1]; + x[3] += x[2]; + x[4] -= x[3] ^ ((~x[2]) >> 23); + x[5] ^= x[4]; + x[6] += x[5]; + x[7] -= x[6] ^ 0x0123456789abcdefULL; +} + +static void doblock(struct tigerhash *th) +{ + int i, o; + unsigned long long x[8], aa, bb, cc; + + for(i = 0; i < 8; i++) { + x[i] = 0; + for(o = 0; o < 8; o++) { + x[i] <<= 8; + x[i] |= th->block[(i * 8) + 7 - o]; + } + } + aa = th->a; + bb = th->b; + cc = th->c; + pass(&th->a, &th->b, &th->c, x, 5); + key_schedule(x); + pass(&th->c, &th->a, &th->b, x, 7); + key_schedule(x); + pass(&th->b, &th->c, &th->a, x, 9); + th->a ^= aa; + th->b -= bb; + th->c += cc; + th->offset = 0; +} + +void dotiger(struct tigerhash *th, char *buf, size_t buflen) +{ + int taken; + + th->len += buflen; + while(buflen > 0) { + taken = buflen; + if(taken > 64 - th->offset) + taken = 64 - th->offset; + memcpy(th->block + th->offset, buf, taken); + th->offset += taken; + buflen -= taken; + buf += taken; + if(th->offset == 64) + doblock(th); + } +} + +void synctiger(struct tigerhash *th) +{ + int i; + unsigned long long buf; + + th->block[th->offset++] = 1; + while(th->offset & 7) + th->block[th->offset++] = 0; + if(th->offset > 56) + doblock(th); + if(th->offset < 56) + memset(th->block + th->offset, 0, 56 - th->offset); + buf = th->len << 3; + for(i = 0; i < 8; i++) { + th->block[56 + i] = buf & 0xff; + buf >>= 8; + } + doblock(th); +} + +void restiger(struct tigerhash *th, char *rbuf) +{ + int i; + unsigned long long buf; + + buf = th->a; + for(i = 0; i < 8; i++) { + rbuf[i] = buf & 0xff; + buf >>= 8; + } + buf = th->b; + for(; i < 16; i++) { + rbuf[i] = buf & 0xff; + buf >>= 8; + } + buf = th->c; + for(; i < 24; i++) { + rbuf[i] = buf & 0xff; + buf >>= 8; + } +} + +void inittigertree(struct tigertreehash *tth) +{ + tth->blocks = 0; + tth->offset = 0; + tth->depth = 0; +} + +static void combine(struct tigertreehash *tth) +{ + struct tigerhash th; + + inittiger(&th); + dotiger(&th, "\001", 1); + tth->depth--; + dotiger(&th, tth->stack[tth->depth - 1], 24); + dotiger(&th, tth->stack[tth->depth], 24); + synctiger(&th); + restiger(&th, tth->stack[tth->depth - 1]); +} + +static void dotreeblock(struct tigertreehash *tth) +{ + struct tigerhash th; + int nb; + + inittiger(&th); + dotiger(&th, "\0", 1); + dotiger(&th, tth->block, tth->offset); + synctiger(&th); + restiger(&th, tth->stack[tth->depth++]); + tth->offset = 0; + for(nb = ++tth->blocks; !(nb & 1); nb >>= 1) + combine(tth); +} + +void dotigertree(struct tigertreehash *tth, char *buf, size_t buflen) +{ + int taken; + + while(buflen > 0) { + taken = buflen; + if(taken > 1024 - tth->offset) + taken = 1024 - tth->offset; + memcpy(tth->block + tth->offset, buf, taken); + tth->offset += taken; + buflen -= taken; + buf += taken; + if(tth->offset == 1024) + dotreeblock(tth); + } +} + +void synctigertree(struct tigertreehash *tth) +{ + if((tth->offset > 0) || (tth->blocks == 0)) + dotreeblock(tth); + while(tth->depth > 1) + combine(tth); +} + +void restigertree(struct tigertreehash *tth, char *rbuf) +{ + memcpy(rbuf, tth->stack[0], 24); +} + +static unsigned long long table[1024] = { + 0x02aab17cf7e90c5eULL, 0xac424b03e243a8ecULL, + 0x72cd5be30dd5fcd3ULL, 0x6d019b93f6f97f3aULL, + 0xcd9978ffd21f9193ULL, 0x7573a1c9708029e2ULL, + 0xb164326b922a83c3ULL, 0x46883eee04915870ULL, + 0xeaace3057103ece6ULL, 0xc54169b808a3535cULL, + 0x4ce754918ddec47cULL, 0x0aa2f4dfdc0df40cULL, + 0x10b76f18a74dbefaULL, 0xc6ccb6235ad1ab6aULL, + 0x13726121572fe2ffULL, 0x1a488c6f199d921eULL, + 0x4bc9f9f4da0007caULL, 0x26f5e6f6e85241c7ULL, + 0x859079dbea5947b6ULL, 0x4f1885c5c99e8c92ULL, + 0xd78e761ea96f864bULL, 0x8e36428c52b5c17dULL, + 0x69cf6827373063c1ULL, 0xb607c93d9bb4c56eULL, + 0x7d820e760e76b5eaULL, 0x645c9cc6f07fdc42ULL, + 0xbf38a078243342e0ULL, 0x5f6b343c9d2e7d04ULL, + 0xf2c28aeb600b0ec6ULL, 0x6c0ed85f7254bcacULL, + 0x71592281a4db4fe5ULL, 0x1967fa69ce0fed9fULL, + 0xfd5293f8b96545dbULL, 0xc879e9d7f2a7600bULL, + 0x860248920193194eULL, 0xa4f9533b2d9cc0b3ULL, + 0x9053836c15957613ULL, 0xdb6dcf8afc357bf1ULL, + 0x18beea7a7a370f57ULL, 0x037117ca50b99066ULL, + 0x6ab30a9774424a35ULL, 0xf4e92f02e325249bULL, + 0x7739db07061ccae1ULL, 0xd8f3b49ceca42a05ULL, + 0xbd56be3f51382f73ULL, 0x45faed5843b0bb28ULL, + 0x1c813d5c11bf1f83ULL, 0x8af0e4b6d75fa169ULL, + 0x33ee18a487ad9999ULL, 0x3c26e8eab1c94410ULL, + 0xb510102bc0a822f9ULL, 0x141eef310ce6123bULL, + 0xfc65b90059ddb154ULL, 0xe0158640c5e0e607ULL, + 0x884e079826c3a3cfULL, 0x930d0d9523c535fdULL, + 0x35638d754e9a2b00ULL, 0x4085fccf40469dd5ULL, + 0xc4b17ad28be23a4cULL, 0xcab2f0fc6a3e6a2eULL, + 0x2860971a6b943fcdULL, 0x3dde6ee212e30446ULL, + 0x6222f32ae01765aeULL, 0x5d550bb5478308feULL, + 0xa9efa98da0eda22aULL, 0xc351a71686c40da7ULL, + 0x1105586d9c867c84ULL, 0xdcffee85fda22853ULL, + 0xccfbd0262c5eef76ULL, 0xbaf294cb8990d201ULL, + 0xe69464f52afad975ULL, 0x94b013afdf133e14ULL, + 0x06a7d1a32823c958ULL, 0x6f95fe5130f61119ULL, + 0xd92ab34e462c06c0ULL, 0xed7bde33887c71d2ULL, + 0x79746d6e6518393eULL, 0x5ba419385d713329ULL, + 0x7c1ba6b948a97564ULL, 0x31987c197bfdac67ULL, + 0xde6c23c44b053d02ULL, 0x581c49fed002d64dULL, + 0xdd474d6338261571ULL, 0xaa4546c3e473d062ULL, + 0x928fce349455f860ULL, 0x48161bbacaab94d9ULL, + 0x63912430770e6f68ULL, 0x6ec8a5e602c6641cULL, + 0x87282515337ddd2bULL, 0x2cda6b42034b701bULL, + 0xb03d37c181cb096dULL, 0xe108438266c71c6fULL, + 0x2b3180c7eb51b255ULL, 0xdf92b82f96c08bbcULL, + 0x5c68c8c0a632f3baULL, 0x5504cc861c3d0556ULL, + 0xabbfa4e55fb26b8fULL, 0x41848b0ab3baceb4ULL, + 0xb334a273aa445d32ULL, 0xbca696f0a85ad881ULL, + 0x24f6ec65b528d56cULL, 0x0ce1512e90f4524aULL, + 0x4e9dd79d5506d35aULL, 0x258905fac6ce9779ULL, + 0x2019295b3e109b33ULL, 0xf8a9478b73a054ccULL, + 0x2924f2f934417eb0ULL, 0x3993357d536d1bc4ULL, + 0x38a81ac21db6ff8bULL, 0x47c4fbf17d6016bfULL, + 0x1e0faadd7667e3f5ULL, 0x7abcff62938beb96ULL, + 0xa78dad948fc179c9ULL, 0x8f1f98b72911e50dULL, + 0x61e48eae27121a91ULL, 0x4d62f7ad31859808ULL, + 0xeceba345ef5ceaebULL, 0xf5ceb25ebc9684ceULL, + 0xf633e20cb7f76221ULL, 0xa32cdf06ab8293e4ULL, + 0x985a202ca5ee2ca4ULL, 0xcf0b8447cc8a8fb1ULL, + 0x9f765244979859a3ULL, 0xa8d516b1a1240017ULL, + 0x0bd7ba3ebb5dc726ULL, 0xe54bca55b86adb39ULL, + 0x1d7a3afd6c478063ULL, 0x519ec608e7669eddULL, + 0x0e5715a2d149aa23ULL, 0x177d4571848ff194ULL, + 0xeeb55f3241014c22ULL, 0x0f5e5ca13a6e2ec2ULL, + 0x8029927b75f5c361ULL, 0xad139fabc3d6e436ULL, + 0x0d5df1a94ccf402fULL, 0x3e8bd948bea5dfc8ULL, + 0xa5a0d357bd3ff77eULL, 0xa2d12e251f74f645ULL, + 0x66fd9e525e81a082ULL, 0x2e0c90ce7f687a49ULL, + 0xc2e8bcbeba973bc5ULL, 0x000001bce509745fULL, + 0x423777bbe6dab3d6ULL, 0xd1661c7eaef06eb5ULL, + 0xa1781f354daacfd8ULL, 0x2d11284a2b16affcULL, + 0xf1fc4f67fa891d1fULL, 0x73ecc25dcb920adaULL, + 0xae610c22c2a12651ULL, 0x96e0a810d356b78aULL, + 0x5a9a381f2fe7870fULL, 0xd5ad62ede94e5530ULL, + 0xd225e5e8368d1427ULL, 0x65977b70c7af4631ULL, + 0x99f889b2de39d74fULL, 0x233f30bf54e1d143ULL, + 0x9a9675d3d9a63c97ULL, 0x5470554ff334f9a8ULL, + 0x166acb744a4f5688ULL, 0x70c74caab2e4aeadULL, + 0xf0d091646f294d12ULL, 0x57b82a89684031d1ULL, + 0xefd95a5a61be0b6bULL, 0x2fbd12e969f2f29aULL, + 0x9bd37013feff9fe8ULL, 0x3f9b0404d6085a06ULL, + 0x4940c1f3166cfe15ULL, 0x09542c4dcdf3defbULL, + 0xb4c5218385cd5ce3ULL, 0xc935b7dc4462a641ULL, + 0x3417f8a68ed3b63fULL, 0xb80959295b215b40ULL, + 0xf99cdaef3b8c8572ULL, 0x018c0614f8fcb95dULL, + 0x1b14accd1a3acdf3ULL, 0x84d471f200bb732dULL, + 0xc1a3110e95e8da16ULL, 0x430a7220bf1a82b8ULL, + 0xb77e090d39df210eULL, 0x5ef4bd9f3cd05e9dULL, + 0x9d4ff6da7e57a444ULL, 0xda1d60e183d4a5f8ULL, + 0xb287c38417998e47ULL, 0xfe3edc121bb31886ULL, + 0xc7fe3ccc980ccbefULL, 0xe46fb590189bfd03ULL, + 0x3732fd469a4c57dcULL, 0x7ef700a07cf1ad65ULL, + 0x59c64468a31d8859ULL, 0x762fb0b4d45b61f6ULL, + 0x155baed099047718ULL, 0x68755e4c3d50baa6ULL, + 0xe9214e7f22d8b4dfULL, 0x2addbf532eac95f4ULL, + 0x32ae3909b4bd0109ULL, 0x834df537b08e3450ULL, + 0xfa209da84220728dULL, 0x9e691d9b9efe23f7ULL, + 0x0446d288c4ae8d7fULL, 0x7b4cc524e169785bULL, + 0x21d87f0135ca1385ULL, 0xcebb400f137b8aa5ULL, + 0x272e2b66580796beULL, 0x3612264125c2b0deULL, + 0x057702bdad1efbb2ULL, 0xd4babb8eacf84be9ULL, + 0x91583139641bc67bULL, 0x8bdc2de08036e024ULL, + 0x603c8156f49f68edULL, 0xf7d236f7dbef5111ULL, + 0x9727c4598ad21e80ULL, 0xa08a0896670a5fd7ULL, + 0xcb4a8f4309eba9cbULL, 0x81af564b0f7036a1ULL, + 0xc0b99aa778199abdULL, 0x959f1ec83fc8e952ULL, + 0x8c505077794a81b9ULL, 0x3acaaf8f056338f0ULL, + 0x07b43f50627a6778ULL, 0x4a44ab49f5eccc77ULL, + 0x3bc3d6e4b679ee98ULL, 0x9cc0d4d1cf14108cULL, + 0x4406c00b206bc8a0ULL, 0x82a18854c8d72d89ULL, + 0x67e366b35c3c432cULL, 0xb923dd61102b37f2ULL, + 0x56ab2779d884271dULL, 0xbe83e1b0ff1525afULL, + 0xfb7c65d4217e49a9ULL, 0x6bdbe0e76d48e7d4ULL, + 0x08df828745d9179eULL, 0x22ea6a9add53bd34ULL, + 0xe36e141c5622200aULL, 0x7f805d1b8cb750eeULL, + 0xafe5c7a59f58e837ULL, 0xe27f996a4fb1c23cULL, + 0xd3867dfb0775f0d0ULL, 0xd0e673de6e88891aULL, + 0x123aeb9eafb86c25ULL, 0x30f1d5d5c145b895ULL, + 0xbb434a2dee7269e7ULL, 0x78cb67ecf931fa38ULL, + 0xf33b0372323bbf9cULL, 0x52d66336fb279c74ULL, + 0x505f33ac0afb4eaaULL, 0xe8a5cd99a2cce187ULL, + 0x534974801e2d30bbULL, 0x8d2d5711d5876d90ULL, + 0x1f1a412891bc038eULL, 0xd6e2e71d82e56648ULL, + 0x74036c3a497732b7ULL, 0x89b67ed96361f5abULL, + 0xffed95d8f1ea02a2ULL, 0xe72b3bd61464d43dULL, + 0xa6300f170bdc4820ULL, 0xebc18760ed78a77aULL, + 0xe6a6be5a05a12138ULL, 0xb5a122a5b4f87c98ULL, + 0x563c6089140b6990ULL, 0x4c46cb2e391f5dd5ULL, + 0xd932addbc9b79434ULL, 0x08ea70e42015aff5ULL, + 0xd765a6673e478cf1ULL, 0xc4fb757eab278d99ULL, + 0xdf11c6862d6e0692ULL, 0xddeb84f10d7f3b16ULL, + 0x6f2ef604a665ea04ULL, 0x4a8e0f0ff0e0dfb3ULL, + 0xa5edeef83dbcba51ULL, 0xfc4f0a2a0ea4371eULL, + 0xe83e1da85cb38429ULL, 0xdc8ff882ba1b1ce2ULL, + 0xcd45505e8353e80dULL, 0x18d19a00d4db0717ULL, + 0x34a0cfeda5f38101ULL, 0x0be77e518887caf2ULL, + 0x1e341438b3c45136ULL, 0xe05797f49089ccf9ULL, + 0xffd23f9df2591d14ULL, 0x543dda228595c5cdULL, + 0x661f81fd99052a33ULL, 0x8736e641db0f7b76ULL, + 0x15227725418e5307ULL, 0xe25f7f46162eb2faULL, + 0x48a8b2126c13d9feULL, 0xafdc541792e76eeaULL, + 0x03d912bfc6d1898fULL, 0x31b1aafa1b83f51bULL, + 0xf1ac2796e42ab7d9ULL, 0x40a3a7d7fcd2ebacULL, + 0x1056136d0afbbcc5ULL, 0x7889e1dd9a6d0c85ULL, + 0xd33525782a7974aaULL, 0xa7e25d09078ac09bULL, + 0xbd4138b3eac6edd0ULL, 0x920abfbe71eb9e70ULL, + 0xa2a5d0f54fc2625cULL, 0xc054e36b0b1290a3ULL, + 0xf6dd59ff62fe932bULL, 0x3537354511a8ac7dULL, + 0xca845e9172fadcd4ULL, 0x84f82b60329d20dcULL, + 0x79c62ce1cd672f18ULL, 0x8b09a2add124642cULL, + 0xd0c1e96a19d9e726ULL, 0x5a786a9b4ba9500cULL, + 0x0e020336634c43f3ULL, 0xc17b474aeb66d822ULL, + 0x6a731ae3ec9baac2ULL, 0x8226667ae0840258ULL, + 0x67d4567691caeca5ULL, 0x1d94155c4875adb5ULL, + 0x6d00fd985b813fdfULL, 0x51286efcb774cd06ULL, + 0x5e8834471fa744afULL, 0xf72ca0aee761ae2eULL, + 0xbe40e4cdaee8e09aULL, 0xe9970bbb5118f665ULL, + 0x726e4beb33df1964ULL, 0x703b000729199762ULL, + 0x4631d816f5ef30a7ULL, 0xb880b5b51504a6beULL, + 0x641793c37ed84b6cULL, 0x7b21ed77f6e97d96ULL, + 0x776306312ef96b73ULL, 0xae528948e86ff3f4ULL, + 0x53dbd7f286a3f8f8ULL, 0x16cadce74cfc1063ULL, + 0x005c19bdfa52c6ddULL, 0x68868f5d64d46ad3ULL, + 0x3a9d512ccf1e186aULL, 0x367e62c2385660aeULL, + 0xe359e7ea77dcb1d7ULL, 0x526c0773749abe6eULL, + 0x735ae5f9d09f734bULL, 0x493fc7cc8a558ba8ULL, + 0xb0b9c1533041ab45ULL, 0x321958ba470a59bdULL, + 0x852db00b5f46c393ULL, 0x91209b2bd336b0e5ULL, + 0x6e604f7d659ef19fULL, 0xb99a8ae2782ccb24ULL, + 0xccf52ab6c814c4c7ULL, 0x4727d9afbe11727bULL, + 0x7e950d0c0121b34dULL, 0x756f435670ad471fULL, + 0xf5add442615a6849ULL, 0x4e87e09980b9957aULL, + 0x2acfa1df50aee355ULL, 0xd898263afd2fd556ULL, + 0xc8f4924dd80c8fd6ULL, 0xcf99ca3d754a173aULL, + 0xfe477bacaf91bf3cULL, 0xed5371f6d690c12dULL, + 0x831a5c285e687094ULL, 0xc5d3c90a3708a0a4ULL, + 0x0f7f903717d06580ULL, 0x19f9bb13b8fdf27fULL, + 0xb1bd6f1b4d502843ULL, 0x1c761ba38fff4012ULL, + 0x0d1530c4e2e21f3bULL, 0x8943ce69a7372c8aULL, + 0xe5184e11feb5ce66ULL, 0x618bdb80bd736621ULL, + 0x7d29bad68b574d0bULL, 0x81bb613e25e6fe5bULL, + 0x071c9c10bc07913fULL, 0xc7beeb7909ac2d97ULL, + 0xc3e58d353bc5d757ULL, 0xeb017892f38f61e8ULL, + 0xd4effb9c9b1cc21aULL, 0x99727d26f494f7abULL, + 0xa3e063a2956b3e03ULL, 0x9d4a8b9a4aa09c30ULL, + 0x3f6ab7d500090fb4ULL, 0x9cc0f2a057268ac0ULL, + 0x3dee9d2dedbf42d1ULL, 0x330f49c87960a972ULL, + 0xc6b2720287421b41ULL, 0x0ac59ec07c00369cULL, + 0xef4eac49cb353425ULL, 0xf450244eef0129d8ULL, + 0x8acc46e5caf4deb6ULL, 0x2ffeab63989263f7ULL, + 0x8f7cb9fe5d7a4578ULL, 0x5bd8f7644e634635ULL, + 0x427a7315bf2dc900ULL, 0x17d0c4aa2125261cULL, + 0x3992486c93518e50ULL, 0xb4cbfee0a2d7d4c3ULL, + 0x7c75d6202c5ddd8dULL, 0xdbc295d8e35b6c61ULL, + 0x60b369d302032b19ULL, 0xce42685fdce44132ULL, + 0x06f3ddb9ddf65610ULL, 0x8ea4d21db5e148f0ULL, + 0x20b0fce62fcd496fULL, 0x2c1b912358b0ee31ULL, + 0xb28317b818f5a308ULL, 0xa89c1e189ca6d2cfULL, + 0x0c6b18576aaadbc8ULL, 0xb65deaa91299fae3ULL, + 0xfb2b794b7f1027e7ULL, 0x04e4317f443b5bebULL, + 0x4b852d325939d0a6ULL, 0xd5ae6beefb207ffcULL, + 0x309682b281c7d374ULL, 0xbae309a194c3b475ULL, + 0x8cc3f97b13b49f05ULL, 0x98a9422ff8293967ULL, + 0x244b16b01076ff7cULL, 0xf8bf571c663d67eeULL, + 0x1f0d6758eee30da1ULL, 0xc9b611d97adeb9b7ULL, + 0xb7afd5887b6c57a2ULL, 0x6290ae846b984fe1ULL, + 0x94df4cdeacc1a5fdULL, 0x058a5bd1c5483affULL, + 0x63166cc142ba3c37ULL, 0x8db8526eb2f76f40ULL, + 0xe10880036f0d6d4eULL, 0x9e0523c9971d311dULL, + 0x45ec2824cc7cd691ULL, 0x575b8359e62382c9ULL, + 0xfa9e400dc4889995ULL, 0xd1823ecb45721568ULL, + 0xdafd983b8206082fULL, 0xaa7d29082386a8cbULL, + 0x269fcd4403b87588ULL, 0x1b91f5f728bdd1e0ULL, + 0xe4669f39040201f6ULL, 0x7a1d7c218cf04adeULL, + 0x65623c29d79ce5ceULL, 0x2368449096c00bb1ULL, + 0xab9bf1879da503baULL, 0xbc23ecb1a458058eULL, + 0x9a58df01bb401eccULL, 0xa070e868a85f143dULL, + 0x4ff188307df2239eULL, 0x14d565b41a641183ULL, + 0xee13337452701602ULL, 0x950e3dcf3f285e09ULL, + 0x59930254b9c80953ULL, 0x3bf299408930da6dULL, + 0xa955943f53691387ULL, 0xa15edecaa9cb8784ULL, + 0x29142127352be9a0ULL, 0x76f0371fff4e7afbULL, + 0x0239f450274f2228ULL, 0xbb073af01d5e868bULL, + 0xbfc80571c10e96c1ULL, 0xd267088568222e23ULL, + 0x9671a3d48e80b5b0ULL, 0x55b5d38ae193bb81ULL, + 0x693ae2d0a18b04b8ULL, 0x5c48b4ecadd5335fULL, + 0xfd743b194916a1caULL, 0x2577018134be98c4ULL, + 0xe77987e83c54a4adULL, 0x28e11014da33e1b9ULL, + 0x270cc59e226aa213ULL, 0x71495f756d1a5f60ULL, + 0x9be853fb60afef77ULL, 0xadc786a7f7443dbfULL, + 0x0904456173b29a82ULL, 0x58bc7a66c232bd5eULL, + 0xf306558c673ac8b2ULL, 0x41f639c6b6c9772aULL, + 0x216defe99fda35daULL, 0x11640cc71c7be615ULL, + 0x93c43694565c5527ULL, 0xea038e6246777839ULL, + 0xf9abf3ce5a3e2469ULL, 0x741e768d0fd312d2ULL, + 0x0144b883ced652c6ULL, 0xc20b5a5ba33f8552ULL, + 0x1ae69633c3435a9dULL, 0x97a28ca4088cfdecULL, + 0x8824a43c1e96f420ULL, 0x37612fa66eeea746ULL, + 0x6b4cb165f9cf0e5aULL, 0x43aa1c06a0abfb4aULL, + 0x7f4dc26ff162796bULL, 0x6cbacc8e54ed9b0fULL, + 0xa6b7ffefd2bb253eULL, 0x2e25bc95b0a29d4fULL, + 0x86d6a58bdef1388cULL, 0xded74ac576b6f054ULL, + 0x8030bdbc2b45805dULL, 0x3c81af70e94d9289ULL, + 0x3eff6dda9e3100dbULL, 0xb38dc39fdfcc8847ULL, + 0x123885528d17b87eULL, 0xf2da0ed240b1b642ULL, + 0x44cefadcd54bf9a9ULL, 0x1312200e433c7ee6ULL, + 0x9ffcc84f3a78c748ULL, 0xf0cd1f72248576bbULL, + 0xec6974053638cfe4ULL, 0x2ba7b67c0cec4e4cULL, + 0xac2f4df3e5ce32edULL, 0xcb33d14326ea4c11ULL, + 0xa4e9044cc77e58bcULL, 0x5f513293d934fcefULL, + 0x5dc9645506e55444ULL, 0x50de418f317de40aULL, + 0x388cb31a69dde259ULL, 0x2db4a83455820a86ULL, + 0x9010a91e84711ae9ULL, 0x4df7f0b7b1498371ULL, + 0xd62a2eabc0977179ULL, 0x22fac097aa8d5c0eULL, + 0xf49fcc2ff1daf39bULL, 0x487fd5c66ff29281ULL, + 0xe8a30667fcdca83fULL, 0x2c9b4be3d2fcce63ULL, + 0xda3ff74b93fbbbc2ULL, 0x2fa165d2fe70ba66ULL, + 0xa103e279970e93d4ULL, 0xbecdec77b0e45e71ULL, + 0xcfb41e723985e497ULL, 0xb70aaa025ef75017ULL, + 0xd42309f03840b8e0ULL, 0x8efc1ad035898579ULL, + 0x96c6920be2b2abc5ULL, 0x66af4163375a9172ULL, + 0x2174abdcca7127fbULL, 0xb33ccea64a72ff41ULL, + 0xf04a4933083066a5ULL, 0x8d970acdd7289af5ULL, + 0x8f96e8e031c8c25eULL, 0xf3fec02276875d47ULL, + 0xec7bf310056190ddULL, 0xf5adb0aebb0f1491ULL, + 0x9b50f8850fd58892ULL, 0x4975488358b74de8ULL, + 0xa3354ff691531c61ULL, 0x0702bbe481d2c6eeULL, + 0x89fb24057deded98ULL, 0xac3075138596e902ULL, + 0x1d2d3580172772edULL, 0xeb738fc28e6bc30dULL, + 0x5854ef8f63044326ULL, 0x9e5c52325add3bbeULL, + 0x90aa53cf325c4623ULL, 0xc1d24d51349dd067ULL, + 0x2051cfeea69ea624ULL, 0x13220f0a862e7e4fULL, + 0xce39399404e04864ULL, 0xd9c42ca47086fcb7ULL, + 0x685ad2238a03e7ccULL, 0x066484b2ab2ff1dbULL, + 0xfe9d5d70efbf79ecULL, 0x5b13b9dd9c481854ULL, + 0x15f0d475ed1509adULL, 0x0bebcd060ec79851ULL, + 0xd58c6791183ab7f8ULL, 0xd1187c5052f3eee4ULL, + 0xc95d1192e54e82ffULL, 0x86eea14cb9ac6ca2ULL, + 0x3485beb153677d5dULL, 0xdd191d781f8c492aULL, + 0xf60866baa784ebf9ULL, 0x518f643ba2d08c74ULL, + 0x8852e956e1087c22ULL, 0xa768cb8dc410ae8dULL, + 0x38047726bfec8e1aULL, 0xa67738b4cd3b45aaULL, + 0xad16691cec0dde19ULL, 0xc6d4319380462e07ULL, + 0xc5a5876d0ba61938ULL, 0x16b9fa1fa58fd840ULL, + 0x188ab1173ca74f18ULL, 0xabda2f98c99c021fULL, + 0x3e0580ab134ae816ULL, 0x5f3b05b773645abbULL, + 0x2501a2be5575f2f6ULL, 0x1b2f74004e7e8ba9ULL, + 0x1cd7580371e8d953ULL, 0x7f6ed89562764e30ULL, + 0xb15926ff596f003dULL, 0x9f65293da8c5d6b9ULL, + 0x6ecef04dd690f84cULL, 0x4782275fff33af88ULL, + 0xe41433083f820801ULL, 0xfd0dfe409a1af9b5ULL, + 0x4325a3342cdb396bULL, 0x8ae77e62b301b252ULL, + 0xc36f9e9f6655615aULL, 0x85455a2d92d32c09ULL, + 0xf2c7dea949477485ULL, 0x63cfb4c133a39ebaULL, + 0x83b040cc6ebc5462ULL, 0x3b9454c8fdb326b0ULL, + 0x56f56a9e87ffd78cULL, 0x2dc2940d99f42bc6ULL, + 0x98f7df096b096e2dULL, 0x19a6e01e3ad852bfULL, + 0x42a99ccbdbd4b40bULL, 0xa59998af45e9c559ULL, + 0x366295e807d93186ULL, 0x6b48181bfaa1f773ULL, + 0x1fec57e2157a0a1dULL, 0x4667446af6201ad5ULL, + 0xe615ebcacfb0f075ULL, 0xb8f31f4f68290778ULL, + 0x22713ed6ce22d11eULL, 0x3057c1a72ec3c93bULL, + 0xcb46acc37c3f1f2fULL, 0xdbb893fd02aaf50eULL, + 0x331fd92e600b9fcfULL, 0xa498f96148ea3ad6ULL, + 0xa8d8426e8b6a83eaULL, 0xa089b274b7735cdcULL, + 0x87f6b3731e524a11ULL, 0x118808e5cbc96749ULL, + 0x9906e4c7b19bd394ULL, 0xafed7f7e9b24a20cULL, + 0x6509eadeeb3644a7ULL, 0x6c1ef1d3e8ef0edeULL, + 0xb9c97d43e9798fb4ULL, 0xa2f2d784740c28a3ULL, + 0x7b8496476197566fULL, 0x7a5be3e6b65f069dULL, + 0xf96330ed78be6f10ULL, 0xeee60de77a076a15ULL, + 0x2b4bee4aa08b9bd0ULL, 0x6a56a63ec7b8894eULL, + 0x02121359ba34fef4ULL, 0x4cbf99f8283703fcULL, + 0x398071350caf30c8ULL, 0xd0a77a89f017687aULL, + 0xf1c1a9eb9e423569ULL, 0x8c7976282dee8199ULL, + 0x5d1737a5dd1f7abdULL, 0x4f53433c09a9fa80ULL, + 0xfa8b0c53df7ca1d9ULL, 0x3fd9dcbc886ccb77ULL, + 0xc040917ca91b4720ULL, 0x7dd00142f9d1dcdfULL, + 0x8476fc1d4f387b58ULL, 0x23f8e7c5f3316503ULL, + 0x032a2244e7e37339ULL, 0x5c87a5d750f5a74bULL, + 0x082b4cc43698992eULL, 0xdf917becb858f63cULL, + 0x3270b8fc5bf86ddaULL, 0x10ae72bb29b5dd76ULL, + 0x576ac94e7700362bULL, 0x1ad112dac61efb8fULL, + 0x691bc30ec5faa427ULL, 0xff246311cc327143ULL, + 0x3142368e30e53206ULL, 0x71380e31e02ca396ULL, + 0x958d5c960aad76f1ULL, 0xf8d6f430c16da536ULL, + 0xc8ffd13f1be7e1d2ULL, 0x7578ae66004ddbe1ULL, + 0x05833f01067be646ULL, 0xbb34b5ad3bfe586dULL, + 0x095f34c9a12b97f0ULL, 0x247ab64525d60ca8ULL, + 0xdcdbc6f3017477d1ULL, 0x4a2e14d4decad24dULL, + 0xbdb5e6d9be0a1eebULL, 0x2a7e70f7794301abULL, + 0xdef42d8a270540fdULL, 0x01078ec0a34c22c1ULL, + 0xe5de511af4c16387ULL, 0x7ebb3a52bd9a330aULL, + 0x77697857aa7d6435ULL, 0x004e831603ae4c32ULL, + 0xe7a21020ad78e312ULL, 0x9d41a70c6ab420f2ULL, + 0x28e06c18ea1141e6ULL, 0xd2b28cbd984f6b28ULL, + 0x26b75f6c446e9d83ULL, 0xba47568c4d418d7fULL, + 0xd80badbfe6183d8eULL, 0x0e206d7f5f166044ULL, + 0xe258a43911cbca3eULL, 0x723a1746b21dc0bcULL, + 0xc7caa854f5d7cdd3ULL, 0x7cac32883d261d9cULL, + 0x7690c26423ba942cULL, 0x17e55524478042b8ULL, + 0xe0be477656a2389fULL, 0x4d289b5e67ab2da0ULL, + 0x44862b9c8fbbfd31ULL, 0xb47cc8049d141365ULL, + 0x822c1b362b91c793ULL, 0x4eb14655fb13dfd8ULL, + 0x1ecbba0714e2a97bULL, 0x6143459d5cde5f14ULL, + 0x53a8fbf1d5f0ac89ULL, 0x97ea04d81c5e5b00ULL, + 0x622181a8d4fdb3f3ULL, 0xe9bcd341572a1208ULL, + 0x1411258643cce58aULL, 0x9144c5fea4c6e0a4ULL, + 0x0d33d06565cf620fULL, 0x54a48d489f219ca1ULL, + 0xc43e5eac6d63c821ULL, 0xa9728b3a72770dafULL, + 0xd7934e7b20df87efULL, 0xe35503b61a3e86e5ULL, + 0xcae321fbc819d504ULL, 0x129a50b3ac60bfa6ULL, + 0xcd5e68ea7e9fb6c3ULL, 0xb01c90199483b1c7ULL, + 0x3de93cd5c295376cULL, 0xaed52edf2ab9ad13ULL, + 0x2e60f512c0a07884ULL, 0xbc3d86a3e36210c9ULL, + 0x35269d9b163951ceULL, 0x0c7d6e2ad0cdb5faULL, + 0x59e86297d87f5733ULL, 0x298ef221898db0e7ULL, + 0x55000029d1a5aa7eULL, 0x8bc08ae1b5061b45ULL, + 0xc2c31c2b6c92703aULL, 0x94cc596baf25ef42ULL, + 0x0a1d73db22540456ULL, 0x04b6a0f9d9c4179aULL, + 0xeffdafa2ae3d3c60ULL, 0xf7c8075bb49496c4ULL, + 0x9cc5c7141d1cd4e3ULL, 0x78bd1638218e5534ULL, + 0xb2f11568f850246aULL, 0xedfabcfa9502bc29ULL, + 0x796ce5f2da23051bULL, 0xaae128b0dc93537cULL, + 0x3a493da0ee4b29aeULL, 0xb5df6b2c416895d7ULL, + 0xfcabbd25122d7f37ULL, 0x70810b58105dc4b1ULL, + 0xe10fdd37f7882a90ULL, 0x524dcab5518a3f5cULL, + 0x3c9e85878451255bULL, 0x4029828119bd34e2ULL, + 0x74a05b6f5d3ceccbULL, 0xb610021542e13ecaULL, + 0x0ff979d12f59e2acULL, 0x6037da27e4f9cc50ULL, + 0x5e92975a0df1847dULL, 0xd66de190d3e623feULL, + 0x5032d6b87b568048ULL, 0x9a36b7ce8235216eULL, + 0x80272a7a24f64b4aULL, 0x93efed8b8c6916f7ULL, + 0x37ddbff44cce1555ULL, 0x4b95db5d4b99bd25ULL, + 0x92d3fda169812fc0ULL, 0xfb1a4a9a90660bb6ULL, + 0x730c196946a4b9b2ULL, 0x81e289aa7f49da68ULL, + 0x64669a0f83b1a05fULL, 0x27b3ff7d9644f48bULL, + 0xcc6b615c8db675b3ULL, 0x674f20b9bcebbe95ULL, + 0x6f31238275655982ULL, 0x5ae488713e45cf05ULL, + 0xbf619f9954c21157ULL, 0xeabac46040a8eae9ULL, + 0x454c6fe9f2c0c1cdULL, 0x419cf6496412691cULL, + 0xd3dc3bef265b0f70ULL, 0x6d0e60f5c3578a9eULL, + 0x5b0e608526323c55ULL, 0x1a46c1a9fa1b59f5ULL, + 0xa9e245a17c4c8ffaULL, 0x65ca5159db2955d7ULL, + 0x05db0a76ce35afc2ULL, 0x81eac77ea9113d45ULL, + 0x528ef88ab6ac0a0dULL, 0xa09ea253597be3ffULL, + 0x430ddfb3ac48cd56ULL, 0xc4b3a67af45ce46fULL, + 0x4ececfd8fbe2d05eULL, 0x3ef56f10b39935f0ULL, + 0x0b22d6829cd619c6ULL, 0x17fd460a74df2069ULL, + 0x6cf8cc8e8510ed40ULL, 0xd6c824bf3a6ecaa7ULL, + 0x61243d581a817049ULL, 0x048bacb6bbc163a2ULL, + 0xd9a38ac27d44cc32ULL, 0x7fddff5baaf410abULL, + 0xad6d495aa804824bULL, 0xe1a6a74f2d8c9f94ULL, + 0xd4f7851235dee8e3ULL, 0xfd4b7f886540d893ULL, + 0x247c20042aa4bfdaULL, 0x096ea1c517d1327cULL, + 0xd56966b4361a6685ULL, 0x277da5c31221057dULL, + 0x94d59893a43acff7ULL, 0x64f0c51ccdc02281ULL, + 0x3d33bcc4ff6189dbULL, 0xe005cb184ce66af1ULL, + 0xff5ccd1d1db99beaULL, 0xb0b854a7fe42980fULL, + 0x7bd46a6a718d4b9fULL, 0xd10fa8cc22a5fd8cULL, + 0xd31484952be4bd31ULL, 0xc7fa975fcb243847ULL, + 0x4886ed1e5846c407ULL, 0x28cddb791eb70b04ULL, + 0xc2b00be2f573417fULL, 0x5c9590452180f877ULL, + 0x7a6bddfff370eb00ULL, 0xce509e38d6d9d6a4ULL, + 0xebeb0f00647fa702ULL, 0x1dcc06cf76606f06ULL, + 0xe4d9f28ba286ff0aULL, 0xd85a305dc918c262ULL, + 0x475b1d8732225f54ULL, 0x2d4fb51668ccb5feULL, + 0xa679b9d9d72bba20ULL, 0x53841c0d912d43a5ULL, + 0x3b7eaa48bf12a4e8ULL, 0x781e0e47f22f1ddfULL, + 0xeff20ce60ab50973ULL, 0x20d261d19dffb742ULL, + 0x16a12b03062a2e39ULL, 0x1960eb2239650495ULL, + 0x251c16fed50eb8b8ULL, 0x9ac0c330f826016eULL, + 0xed152665953e7671ULL, 0x02d63194a6369570ULL, + 0x5074f08394b1c987ULL, 0x70ba598c90b25ce1ULL, + 0x794a15810b9742f6ULL, 0x0d5925e9fcaf8c6cULL, + 0x3067716cd868744eULL, 0x910ab077e8d7731bULL, + 0x6a61bbdb5ac42f61ULL, 0x93513efbf0851567ULL, + 0xf494724b9e83e9d5ULL, 0xe887e1985c09648dULL, + 0x34b1d3c675370cfdULL, 0xdc35e433bc0d255dULL, + 0xd0aab84234131be0ULL, 0x08042a50b48b7eafULL, + 0x9997c4ee44a3ab35ULL, 0x829a7b49201799d0ULL, + 0x263b8307b7c54441ULL, 0x752f95f4fd6a6ca6ULL, + 0x927217402c08c6e5ULL, 0x2a8ab754a795d9eeULL, + 0xa442f7552f72943dULL, 0x2c31334e19781208ULL, + 0x4fa98d7ceaee6291ULL, 0x55c3862f665db309ULL, + 0xbd0610175d53b1f3ULL, 0x46fe6cb840413f27ULL, + 0x3fe03792df0cfa59ULL, 0xcfe700372eb85e8fULL, + 0xa7be29e7adbce118ULL, 0xe544ee5cde8431ddULL, + 0x8a781b1b41f1873eULL, 0xa5c94c78a0d2f0e7ULL, + 0x39412e2877b60728ULL, 0xa1265ef3afc9a62cULL, + 0xbcc2770c6a2506c5ULL, 0x3ab66dd5dce1ce12ULL, + 0xe65499d04a675b37ULL, 0x7d8f523481bfd216ULL, + 0x0f6f64fcec15f389ULL, 0x74efbe618b5b13c8ULL, + 0xacdc82b714273e1dULL, 0xdd40bfe003199d17ULL, + 0x37e99257e7e061f8ULL, 0xfa52626904775aaaULL, + 0x8bbbf63a463d56f9ULL, 0xf0013f1543a26e64ULL, + 0xa8307e9f879ec898ULL, 0xcc4c27a4150177ccULL, + 0x1b432f2cca1d3348ULL, 0xde1d1f8f9f6fa013ULL, + 0x606602a047a7ddd6ULL, 0xd237ab64cc1cb2c7ULL, + 0x9b938e7225fcd1d3ULL, 0xec4e03708e0ff476ULL, + 0xfeb2fbda3d03c12dULL, 0xae0bced2ee43889aULL, + 0x22cb8923ebfb4f43ULL, 0x69360d013cf7396dULL, + 0x855e3602d2d4e022ULL, 0x073805bad01f784cULL, + 0x33e17a133852f546ULL, 0xdf4874058ac7b638ULL, + 0xba92b29c678aa14aULL, 0x0ce89fc76cfaadcdULL, + 0x5f9d4e0908339e34ULL, 0xf1afe9291f5923b9ULL, + 0x6e3480f60f4a265fULL, 0xeebf3a2ab29b841cULL, + 0xe21938a88f91b4adULL, 0x57dfeff845c6d3c3ULL, + 0x2f006b0bf62caaf2ULL, 0x62f479ef6f75ee78ULL, + 0x11a55ad41c8916a9ULL, 0xf229d29084fed453ULL, + 0x42f1c27b16b000e6ULL, 0x2b1f76749823c074ULL, + 0x4b76eca3c2745360ULL, 0x8c98f463b91691bdULL, + 0x14bcc93cf1ade66aULL, 0x8885213e6d458397ULL, + 0x8e177df0274d4711ULL, 0xb49b73b5503f2951ULL, + 0x10168168c3f96b6bULL, 0x0e3d963b63cab0aeULL, + 0x8dfc4b5655a1db14ULL, 0xf789f1356e14de5cULL, + 0x683e68af4e51dac1ULL, 0xc9a84f9d8d4b0fd9ULL, + 0x3691e03f52a0f9d1ULL, 0x5ed86e46e1878e80ULL, + 0x3c711a0e99d07150ULL, 0x5a0865b20c4e9310ULL, + 0x56fbfc1fe4f0682eULL, 0xea8d5de3105edf9bULL, + 0x71abfdb12379187aULL, 0x2eb99de1bee77b9cULL, + 0x21ecc0ea33cf4523ULL, 0x59a4d7521805c7a1ULL, + 0x3896f5eb56ae7c72ULL, 0xaa638f3db18f75dcULL, + 0x9f39358dabe9808eULL, 0xb7defa91c00b72acULL, + 0x6b5541fd62492d92ULL, 0x6dc6dee8f92e4d5bULL, + 0x353f57abc4beea7eULL, 0x735769d6da5690ceULL, + 0x0a234aa642391484ULL, 0xf6f9508028f80d9dULL, + 0xb8e319a27ab3f215ULL, 0x31ad9c1151341a4dULL, + 0x773c22a57bef5805ULL, 0x45c7561a07968633ULL, + 0xf913da9e249dbe36ULL, 0xda652d9b78a64c68ULL, + 0x4c27a97f3bc334efULL, 0x76621220e66b17f4ULL, + 0x967743899acd7d0bULL, 0xf3ee5bcae0ed6782ULL, + 0x409f753600c879fcULL, 0x06d09a39b5926db6ULL, + 0x6f83aeb0317ac588ULL, 0x01e6ca4a86381f21ULL, + 0x66ff3462d19f3025ULL, 0x72207c24ddfd3bfbULL, + 0x4af6b6d3e2ece2ebULL, 0x9c994dbec7ea08deULL, + 0x49ace597b09a8bc4ULL, 0xb38c4766cf0797baULL, + 0x131b9373c57c2a75ULL, 0xb1822cce61931e58ULL, + 0x9d7555b909ba1c0cULL, 0x127fafdd937d11d2ULL, + 0x29da3badc66d92e4ULL, 0xa2c1d57154c2ecbcULL, + 0x58c5134d82f6fe24ULL, 0x1c3ae3515b62274fULL, + 0xe907c82e01cb8126ULL, 0xf8ed091913e37fcbULL, + 0x3249d8f9c80046c9ULL, 0x80cf9bede388fb63ULL, + 0x1881539a116cf19eULL, 0x5103f3f76bd52457ULL, + 0x15b7e6f5ae47f7a8ULL, 0xdbd7c6ded47e9ccfULL, + 0x44e55c410228bb1aULL, 0xb647d4255edb4e99ULL, + 0x5d11882bb8aafc30ULL, 0xf5098bbb29d3212aULL, + 0x8fb5ea14e90296b3ULL, 0x677b942157dd025aULL, + 0xfb58e7c0a390acb5ULL, 0x89d3674c83bd4a01ULL, + 0x9e2da4df4bf3b93bULL, 0xfcc41e328cab4829ULL, + 0x03f38c96ba582c52ULL, 0xcad1bdbd7fd85db2ULL, + 0xbbb442c16082ae83ULL, 0xb95fe86ba5da9ab0ULL, + 0xb22e04673771a93fULL, 0x845358c9493152d8ULL, + 0xbe2a488697b4541eULL, 0x95a2dc2dd38e6966ULL, + 0xc02c11ac923c852bULL, 0x2388b1990df2a87bULL, + 0x7c8008fa1b4f37beULL, 0x1f70d0c84d54e503ULL, + 0x5490adec7ece57d4ULL, 0x002b3c27d9063a3aULL, + 0x7eaea3848030a2bfULL, 0xc602326ded2003c0ULL, + 0x83a7287d69a94086ULL, 0xc57a5fcb30f57a8aULL, + 0xb56844e479ebe779ULL, 0xa373b40f05dcbce9ULL, + 0xd71a786e88570ee2ULL, 0x879cbacdbde8f6a0ULL, + 0x976ad1bcc164a32fULL, 0xab21e25e9666d78bULL, + 0x901063aae5e5c33cULL, 0x9818b34448698d90ULL, + 0xe36487ae3e1e8abbULL, 0xafbdf931893bdcb4ULL, + 0x6345a0dc5fbbd519ULL, 0x8628fe269b9465caULL, + 0x1e5d01603f9c51ecULL, 0x4de44006a15049b7ULL, + 0xbf6c70e5f776cbb1ULL, 0x411218f2ef552bedULL, + 0xcb0c0708705a36a3ULL, 0xe74d14754f986044ULL, + 0xcd56d9430ea8280eULL, 0xc12591d7535f5065ULL, + 0xc83223f1720aef96ULL, 0xc3a0396f7363a51fULL +}; diff --git a/daemon/tiger.h b/daemon/tiger.h new file mode 100644 index 0000000..a6322ff --- /dev/null +++ b/daemon/tiger.h @@ -0,0 +1,28 @@ +#ifndef _TIGER_H +#define _TIGER_H + +struct tigerhash { + unsigned long long a, b, c; + unsigned char block[64]; + int offset; + size_t len; +}; + +struct tigertreehash { + int blocks; + char block[1024]; + int offset; + char stack[64][24]; + int depth; +}; + +void inittiger(struct tigerhash *th); +void dotiger(struct tigerhash *th, char *buf, size_t buflen); +void synctiger(struct tigerhash *th); +void restiger(struct tigerhash *th, char *rbuf); +void inittigertree(struct tigertreehash *tth); +void dotigertree(struct tigertreehash *tth, char *buf, size_t buflen); +void synctigertree(struct tigertreehash *tth); +void restigertree(struct tigertreehash *tth, char *rbuf); + +#endif diff --git a/daemon/transfer.c b/daemon/transfer.c new file mode 100644 index 0000000..15bd5d2 --- /dev/null +++ b/daemon/transfer.c @@ -0,0 +1,738 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "log.h" +#include "utils.h" +#include "sysevents.h" +#include "auth.h" +#include "transfer.h" +#include "module.h" + +static void killfilter(struct transfer *transfer); + +struct transfer *transfers = NULL; +int numtransfers = 0; +GCBCHAIN(newtransfercb, struct transfer *); + +void freetransfer(struct transfer *transfer) +{ + struct transarg *ta; + + if(transfer == transfers) + transfers = transfer->next; + if(transfer->next != NULL) + transfer->next->prev = transfer->prev; + if(transfer->prev != NULL) + transfer->prev->next = transfer->next; + CBCHAINDOCB(transfer, trans_destroy, transfer); + CBCHAINFREE(transfer, trans_ac); + CBCHAINFREE(transfer, trans_act); + CBCHAINFREE(transfer, trans_p); + CBCHAINFREE(transfer, trans_destroy); + CBCHAINFREE(transfer, trans_filterout); + while((ta = transfer->args) != NULL) + { + transfer->args = ta->next; + free(ta->rec); + free(ta->val); + free(ta); + } + if(transfer->filter != -1) + killfilter(transfer); + if(transfer->etimer != NULL) + canceltimer(transfer->etimer); + if(transfer->auth != NULL) + authputhandle(transfer->auth); + if(transfer->peerid != NULL) + free(transfer->peerid); + if(transfer->peernick != NULL) + free(transfer->peernick); + if(transfer->path != NULL) + free(transfer->path); + if(transfer->actdesc != NULL) + free(transfer->actdesc); + if(transfer->filterbuf != NULL) + free(transfer->filterbuf); + if(transfer->localend != NULL) + { + transfer->localend->readcb = NULL; + transfer->localend->writecb = NULL; + transfer->localend->errcb = NULL; + putsock(transfer->localend); + } + if(transfer->filterout != NULL) + { + transfer->filterout->readcb = NULL; + transfer->filterout->writecb = NULL; + transfer->filterout->errcb = NULL; + putsock(transfer->filterout); + } + if(transfer->fn != NULL) + putfnetnode(transfer->fn); + free(transfer); + numtransfers--; +} + +struct transfer *newtransfer(void) +{ + struct transfer *new; + static int curid = 0; + + new = smalloc(sizeof(*new)); + memset(new, 0, sizeof(*new)); + new->id = curid++; + new->size = -1; + new->endpos = -1; + new->filter = -1; + CBCHAININIT(new, trans_ac); + CBCHAININIT(new, trans_act); + CBCHAININIT(new, trans_p); + CBCHAININIT(new, trans_destroy); + CBCHAININIT(new, trans_filterout); + new->next = NULL; + new->prev = NULL; + time(&new->activity); + numtransfers++; + return(new); +} + +void transferaddarg(struct transfer *transfer, wchar_t *rec, wchar_t *val) +{ + struct transarg *ta; + + ta = smalloc(sizeof(*ta)); + ta->rec = swcsdup(rec); + ta->val = swcsdup(val); + ta->next = transfer->args; + transfer->args = ta; +} + +void transferattach(struct transfer *transfer, struct transferiface *iface, void *data) +{ + if(transfer->iface != NULL) + transferdetach(transfer); + transfer->iface = iface; + transfer->ifacedata = data; +} + +void transferdetach(struct transfer *transfer) +{ + if(transfer->iface != NULL) + { + transfer->iface->detach(transfer, transfer->ifacedata); + transfer->iface = NULL; + transfer->ifacedata = NULL; + } +} + +struct transfer *newupload(struct fnetnode *fn, struct fnet *fnet, wchar_t *nickid, struct transferiface *iface, void *data) +{ + struct transfer *transfer; + + transfer = newtransfer(); + if(fnet != NULL) + transfer->fnet = fnet; + else + transfer->fnet = fn->fnet; + transfer->peerid = swcsdup(nickid); + transfer->state = TRNS_HS; + transfer->dir = TRNSD_UP; + if(fn != NULL) + getfnetnode(transfer->fn = fn); + transferattach(transfer, iface, data); + linktransfer(transfer); + bumptransfer(transfer); + return(transfer); +} + +void linktransfer(struct transfer *transfer) +{ + transfer->next = transfers; + transfer->prev = NULL; + if(transfers != NULL) + transfers->prev = transfer; + transfers = transfer; + GCBCHAINDOCB(newtransfercb, transfer); +} + +void resettransfer(struct transfer *transfer) +{ + if(transfer->dir == TRNSD_DOWN) + { + if(transfer->iface != NULL) + transferdetach(transfer); + killfilter(transfer); + transfersetstate(transfer, TRNS_WAITING); + transfersetactivity(transfer, L"reset"); + return; + } +} + +struct transfer *findtransfer(int id) +{ + struct transfer *transfer; + + for(transfer = transfers; transfer != NULL; transfer = transfer->next) + { + if(transfer->id == id) + break; + } + return(transfer); +} + +static void transexpire(int cancelled, struct transfer *transfer) +{ + transfer->etimer = NULL; + if(!cancelled) + bumptransfer(transfer); + else + transfer->timeout = 0; +} + +static void transferread(struct socket *sk, struct transfer *transfer) +{ + if(sockgetdatalen(sk) >= 65536) + sk->ignread = 1; + if((transfer->iface != NULL) && (transfer->iface->gotdata != NULL)) + transfer->iface->gotdata(transfer, transfer->ifacedata); +} + +static void transferwrite(struct socket *sk, struct transfer *transfer) +{ + if((transfer->iface != NULL) && (transfer->iface->wantdata != NULL)) + transfer->iface->wantdata(transfer, transfer->ifacedata); +} + +static void transfererr(struct socket *sk, int errno, struct transfer *transfer) +{ + if((transfer->iface != NULL) && (transfer->iface->endofdata != NULL)) + transfer->iface->endofdata(transfer, transfer->ifacedata); +} + +void transferputdata(struct transfer *transfer, void *buf, size_t size) +{ + time(&transfer->activity); + sockqueue(transfer->localend, buf, size); + transfer->curpos += size; + CBCHAINDOCB(transfer, trans_p, transfer); +} + +void transferendofdata(struct transfer *transfer) +{ + if(transfer->curpos >= transfer->size) + { + transfersetstate(transfer, TRNS_DONE); + transfer->localend->readcb = NULL; + transfer->localend->writecb = NULL; + transfer->localend->errcb = NULL; + putsock(transfer->localend); + transfer->localend = NULL; + } else { + resettransfer(transfer); + } +} + +size_t transferdatasize(struct transfer *transfer) +{ + return(sockqueuesize(transfer->localend)); +} + +void *transfergetdata(struct transfer *transfer, size_t *size) +{ + void *buf; + + if(transfer->localend == NULL) + return(NULL); + transfer->localend->ignread = 0; + time(&transfer->activity); + if((buf = sockgetinbuf(transfer->localend, size)) == NULL) + return(NULL); + if((transfer->endpos >= 0) && (transfer->curpos + *size >= transfer->endpos)) + { + *size = transfer->endpos - transfer->curpos; + buf = srealloc(buf, *size); + } + transfer->curpos += *size; + CBCHAINDOCB(transfer, trans_p, transfer); + return(buf); +} + +void transferprepul(struct transfer *transfer, size_t size, size_t start, size_t end, struct socket *lesk) +{ + transfersetsize(transfer, size); + transfer->curpos = start; + transfer->endpos = end; + lesk->ignread = 1; + transfersetlocalend(transfer, lesk); +} + +void transferstartul(struct transfer *transfer, struct socket *sk) +{ + transfersetstate(transfer, TRNS_MAIN); + socksettos(sk, confgetint("transfer", "ultos")); + if(transfer->localend != NULL) + transfer->localend->ignread = 0; +} + +void transfersetlocalend(struct transfer *transfer, struct socket *sk) +{ + if(transfer->localend != NULL) + putsock(transfer->localend); + getsock(transfer->localend = sk); + sk->data = transfer; + sk->readcb = (void (*)(struct socket *, void *))transferread; + sk->writecb = (void (*)(struct socket *, void *))transferwrite; + sk->errcb = (void (*)(struct socket *, int, void *))transfererr; +} + +void bumptransfer(struct transfer *transfer) +{ + struct fnetnode *fn; + struct fnetpeer *peer; + time_t now; + + if((now = time(NULL)) < transfer->timeout) + { + if(transfer->etimer == NULL) + transfer->etimer = timercallback(transfer->timeout, (void (*)(int, void *))transexpire, transfer); + return; + } + if(transfer->etimer != NULL) + canceltimer(transfer->etimer); + switch(transfer->state) + { + case TRNS_WAITING: + if(transfer->fn != NULL) + { + fn = transfer->fn; + if(fn->state != FNN_EST) + { + transfer->close = 1; + return; + } + peer = fnetfindpeer(fn, transfer->peerid); + } else { + peer = NULL; + for(fn = fnetnodes; fn != NULL; fn = fn->next) + { + if((fn->state == FNN_EST) && (fn->fnet == transfer->fnet) && ((peer = fnetfindpeer(fn, transfer->peerid)) != NULL)) + break; + } + } + transfer->etimer = timercallback(transfer->timeout = (time(NULL) + 30), (void (*)(int, void *))transexpire, transfer); + if(now - transfer->lastreq > 30) + { + if(peer != NULL) + { + fn->fnet->reqconn(peer); + time(&transfer->lastreq); + } + } + break; + case TRNS_HS: + if(transfer->dir == TRNSD_UP) + { + if(now - transfer->activity < 60) + transfer->etimer = timercallback(transfer->timeout = (time(NULL) + 60), (void (*)(int, void *))transexpire, transfer); + else + transfer->close = 1; + } else if(transfer->dir == TRNSD_DOWN) { + if(now - transfer->activity < 60) + transfer->etimer = timercallback(transfer->timeout = (time(NULL) + 60), (void (*)(int, void *))transexpire, transfer); + else + resettransfer(transfer); + } + break; + case TRNS_MAIN: + if(transfer->dir == TRNSD_UP) + { + if(now - transfer->activity < 300) + transfer->etimer = timercallback(transfer->timeout = (time(NULL) + 300), (void (*)(int, void *))transexpire, transfer); + else + transfer->close = 1; + } + break; + } +} + +void transfersetactivity(struct transfer *transfer, wchar_t *desc) +{ + time(&transfer->activity); + if(desc != NULL) + { + if(transfer->actdesc != NULL) + free(transfer->actdesc); + transfer->actdesc = swcsdup(desc); + } + bumptransfer(transfer); + CBCHAINDOCB(transfer, trans_act, transfer); +} + +void transfersetstate(struct transfer *transfer, int newstate) +{ + transfer->state = newstate; + if(transfer->etimer != NULL) + canceltimer(transfer->etimer); + transfersetactivity(transfer, NULL); + CBCHAINDOCB(transfer, trans_ac, transfer, L"state"); +} + +void transfersetnick(struct transfer *transfer, wchar_t *newnick) +{ + if(transfer->peernick != NULL) + free(transfer->peernick); + transfer->peernick = swcsdup(newnick); + CBCHAINDOCB(transfer, trans_ac, transfer, L"nick"); +} + +void transfersetsize(struct transfer *transfer, int newsize) +{ + transfer->size = newsize; + CBCHAINDOCB(transfer, trans_ac, transfer, L"size"); +} + +void transferseterror(struct transfer *transfer, int error) +{ + transfer->error = error; + CBCHAINDOCB(transfer, trans_ac, transfer, L"error"); +} + +void transfersetpath(struct transfer *transfer, wchar_t *path) +{ + if(transfer->path != NULL) + free(transfer->path); + transfer->path = swcsdup(path); + CBCHAINDOCB(transfer, trans_ac, transfer, L"path"); +} + +int slotsleft(void) +{ + struct transfer *transfer; + int slots; + + slots = confgetint("transfer", "slots"); + for(transfer = transfers; (transfer != NULL) && (slots > 0); transfer = transfer->next) + { + if((transfer->dir == TRNSD_UP) && (transfer->state == TRNS_MAIN) && !transfer->flags.b.minislot) + slots--; + } + return(slots); +} + +static void killfilter(struct transfer *transfer) +{ + if(transfer->filter != -1) + { + kill(-transfer->filter, SIGHUP); + transfer->filter = -1; + } + if(transfer->localend) + { + transfer->localend->readcb = NULL; + transfer->localend->writecb = NULL; + transfer->localend->errcb = NULL; + putsock(transfer->localend); + transfer->localend = NULL; + } + if(transfer->filterout) + { + transfer->filterout->readcb = NULL; + putsock(transfer->filterout); + transfer->filterout = NULL; + } + if(transfer->filterbuf) + { + free(transfer->filterbuf); + transfer->filterbuf = NULL; + } + transfer->filterbufsize = transfer->filterbufdata = 0; +} + +static char *findfilter(struct passwd *pwd) +{ + char *path, *filtername; + + if((path = sprintf2("%s/.dcdl-filter", pwd->pw_dir)) != NULL) + { + if(!access(path, X_OK)) + return(path); + free(path); + } + if((filtername = icwcstombs(confgetstr("transfer", "filter"), NULL)) == NULL) + { + flog(LOG_WARNING, "could not convert filter name into local charset: %s", strerror(errno)); + } else { + if(strchr(filtername, '/') == NULL) + { + if((path = sprintf2("/etc/%s", filtername)) != NULL) + { + if(!access(path, X_OK)) + { + free(filtername); + return(path); + } + free(path); + } + if((path = sprintf2("/usr/etc/%s", filtername)) != NULL) + { + if(!access(path, X_OK)) + { + free(filtername); + return(path); + } + free(path); + } + if((path = sprintf2("/usr/local/etc/%s", filtername)) != NULL) + { + if(!access(path, X_OK)) + { + free(filtername); + return(path); + } + free(path); + } + } else { + if(!access(filtername, X_OK)) + return(filtername); + } + free(filtername); + } + return(NULL); +} + +static void filterread(struct socket *sk, struct transfer *transfer) +{ + char *buf, *p, *p2; + size_t bufsize; + wchar_t *cmd, *arg; + + if((buf = sockgetinbuf(sk, &bufsize)) == NULL) + return; + bufcat(transfer->filterbuf, buf, bufsize); + free(buf); + if((p = memchr(transfer->filterbuf, '\n', transfer->filterbufdata)) != NULL) + { + *(p++) = 0; + if((p2 = strchr(transfer->filterbuf, ' ')) != NULL) + *(p2++) = 0; + if((cmd = icmbstowcs(transfer->filterbuf, NULL)) != NULL) + { + arg = NULL; + if(p2 != NULL) + { + if((arg = icmbstowcs(p2, NULL)) == NULL) + flog(LOG_WARNING, "filter sent a string which could not be converted into the local charset: %s: %s", transfer->filterbuf, strerror(errno)); + } + CBCHAINDOCB(transfer, trans_filterout, transfer, cmd, arg); + if(arg != NULL) + free(arg); + free(cmd); + } else { + flog(LOG_WARNING, "filter sent a string which could not be converted into the local charset: %s: %s", transfer->filterbuf, strerror(errno)); + } + memmove(transfer->filterbuf, p, transfer->filterbufdata -= (p - transfer->filterbuf)); + } +} + +static void filterexit(pid_t pid, int status, void *data) +{ + struct transfer *transfer; + + for(transfer = transfers; transfer != NULL; transfer = transfer->next) + { + if(transfer->filter == pid) + { + transfer->filter = -1; + killfilter(transfer); + if(WEXITSTATUS(status)) + { + resettransfer(transfer); + } else { + freetransfer(transfer); + } + break; + } + } +} + +int forkfilter(struct transfer *transfer) +{ + char *filtername, *filename, *peerid, *buf; + wchar_t *wfilename; + struct passwd *pwent; + pid_t pid; + int inpipe, outpipe; + char **argv; + size_t argvsize, argvdata; + struct socket *insock, *outsock; + struct transarg *ta; + char *rec, *val; + + wfilename = transfer->path; + if(transfer->fnet->filebasename != NULL) + wfilename = transfer->fnet->filebasename(wfilename); + if(transfer->auth == NULL) + { + flog(LOG_WARNING, "tried to fork filter for transfer with NULL authhandle (tranfer %i)", transfer->id); + errno = EACCES; + return(-1); + } + if((pwent = getpwuid(transfer->owner)) == NULL) + { + flog(LOG_WARNING, "no passwd entry for uid %i (found in transfer %i)", transfer->owner, transfer->id); + errno = EACCES; + return(-1); + } + if((filtername = findfilter(pwent)) == NULL) + { + flog(LOG_WARNING, "could not find filter for user %s", pwent->pw_name); + errno = ENOENT; + return(-1); + } + if((filename = icwcstombs(wfilename, NULL)) == NULL) + { + if((buf = icwcstombs(wfilename, "UTF-8")) == NULL) + { + flog(LOG_WARNING, "could convert transfer filename to neither local charset nor UTF-8: %s", strerror(errno)); + return(-1); + } + filename = sprintf2("utf8-%s", buf); + free(buf); + } + if((peerid = icwcstombs(transfer->peerid, NULL)) == NULL) + { + if((buf = icwcstombs(transfer->peerid, "UTF-8")) == NULL) + { + flog(LOG_WARNING, "could convert transfer peerid to neither local charset nor UTF-8: %s", strerror(errno)); + free(filename); + return(-1); + } + peerid = sprintf2("utf8-%s", buf); + free(buf); + } + if((pid = forksess(transfer->owner, transfer->auth, filterexit, NULL, FD_PIPE, 0, O_WRONLY, &inpipe, FD_PIPE, 1, O_RDONLY, &outpipe, FD_FILE, 2, O_RDWR, "/dev/null", FD_END)) < 0) + { + flog(LOG_WARNING, "could not fork session for filter for transfer %i: %s", transfer->id, strerror(errno)); + return(-1); + } + if(pid == 0) + { + argv = NULL; + argvsize = argvdata = 0; + buf = sprintf2("%i", transfer->size); + addtobuf(argv, filtername); + addtobuf(argv, filename); + addtobuf(argv, buf); + addtobuf(argv, peerid); + for(ta = transfer->args; ta != NULL; ta = ta->next) + { + if((rec = icwcstombs(ta->rec, NULL)) == NULL) + continue; + if((val = icwcstombs(ta->val, NULL)) == NULL) + continue; + addtobuf(argv, rec); + addtobuf(argv, val); + } + addtobuf(argv, NULL); + execv(filtername, argv); + flog(LOG_WARNING, "could not exec filter %s: %s", filtername, strerror(errno)); + exit(127); + } + insock = wrapsock(inpipe); + outsock = wrapsock(outpipe); + /* Really, really strange thing here - sometimes the kernel would + * return POLLIN on insock, even though it's a write-side + * pipe. The corresponding read on the pipe naturally returns + * EBADF, causing doldacond to think there's something wrong with + * the fd, and thus it closes it. Until I can find out whyever the + * kernel gives a POLLIN on the fd (if I can at all...), I'll just + * set ignread on insock for now. */ + insock->ignread = 1; + transfer->filter = pid; + transfersetlocalend(transfer, insock); + getsock(transfer->filterout = outsock); + outsock->data = transfer; + outsock->readcb = (void (*)(struct socket *, void *))filterread; + putsock(insock); + putsock(outsock); + free(filtername); + free(filename); + free(peerid); + return(0); +} + +static int run(void) +{ + struct transfer *transfer, *next; + + for(transfer = transfers; transfer != NULL; transfer = transfer->next) + { + if((transfer->endpos >= 0) && (transfer->state == TRNS_MAIN) && (transfer->localend != NULL) && (transfer->localend->state == SOCK_EST) && (transfer->curpos >= transfer->endpos)) + { + if((transfer->iface != NULL) && (transfer->iface->endofdata != NULL)) + transfer->iface->endofdata(transfer, transfer->ifacedata); + closesock(transfer->localend); + } + } + for(transfer = transfers; transfer != NULL; transfer = next) + { + next = transfer->next; + if(transfer->close) + { + transferdetach(transfer); + freetransfer(transfer); + continue; + } + } + return(0); +} + +static struct configvar myvars[] = +{ + {CONF_VAR_INT, "slots", {.num = 3}}, + {CONF_VAR_INT, "ultos", {.num = SOCK_TOS_MAXTP}}, + {CONF_VAR_INT, "dltos", {.num = SOCK_TOS_MAXTP}}, + {CONF_VAR_STRING, "filter", {.str = L"dc-filter"}}, + {CONF_VAR_END} +}; + +static struct module me = +{ + .conf = + { + .vars = myvars + }, + .name = "transfer", + .run = run +}; + +MODULE(me); diff --git a/daemon/transfer.h b/daemon/transfer.h new file mode 100644 index 0000000..af50310 --- /dev/null +++ b/daemon/transfer.h @@ -0,0 +1,129 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _TRANSFER_H +#define _TRANSFER_H + +#include +#include + +#include "net.h" +#include "filenet.h" +#include "utils.h" +#include "auth.h" + +#define TRNS_WAITING 0 +#define TRNS_HS 1 +#define TRNS_MAIN 2 +#define TRNS_DONE 3 + +#define TRNSD_UNKNOWN 0 +#define TRNSD_UP 1 +#define TRNSD_DOWN 2 + +#define TRNSE_NOERROR 0 +#define TRNSE_NOTFOUND 1 +#define TRNSE_NOSLOTS 2 + +struct transfer; + +struct transferiface +{ + void (*detach)(struct transfer *transfer, void *data); + void (*gotdata)(struct transfer *transfer, void *data); + void (*endofdata)(struct transfer *transfer, void *data); + void (*wantdata)(struct transfer *transfer, void *data); +}; + +struct transarg +{ + struct transarg *next; + wchar_t *rec; + wchar_t *val; +}; + +struct transfer +{ + struct transfer *next, *prev; + int id, close; + union + { + int w; + struct + { + int byop:1; + int sgranted:1; + int minislot:1; + } b; + } flags; + struct timer *etimer; + time_t timeout, activity, lastreq; + wchar_t *actdesc; + struct fnet *fnet; + struct transferiface *iface; + wchar_t *peerid, *peernick; + wchar_t *path; + uid_t owner; + int state, dir, error; + size_t size, curpos, endpos; + struct fnetnode *fn; + void *ifacedata; + struct socket *localend; + struct transarg *args; + pid_t filter; + struct authhandle *auth; + struct socket *filterout; + char *filterbuf; + size_t filterbufsize, filterbufdata; + CBCHAIN(trans_ac, struct transfer *transfer, wchar_t *attrib); + CBCHAIN(trans_p, struct transfer *transfer); + CBCHAIN(trans_act, struct transfer *transfer); + CBCHAIN(trans_destroy, struct transfer *transfer); + CBCHAIN(trans_filterout, struct transfer *transfer, wchar_t *cmd, wchar_t *arg); +}; + +void freetransfer(struct transfer *transfer); +struct transfer *newtransfer(void); +void linktransfer(struct transfer *transfer); +int slotsleft(void); +void bumptransfer(struct transfer *transfer); +struct transfer *findtransfer(int id); +struct transfer *newupload(struct fnetnode *fn, struct fnet *fnet, wchar_t *nickid, struct transferiface *iface, void *data); +void transfersetnick(struct transfer *transfer, wchar_t *newnick); +void transfersetpath(struct transfer *transfer, wchar_t *newpath); +void transfersetstate(struct transfer *transfer, int newstate); +void transfersetsize(struct transfer *transfer, int newsize); +void transferseterror(struct transfer *transfer, int error); +void transfersetactivity(struct transfer *transfer, wchar_t *desc); +void transferattach(struct transfer *transfer, struct transferiface *iface, void *data); +void transferdetach(struct transfer *transfer); +void resettransfer(struct transfer *transfer); +void transfersetlocalend(struct transfer *transfer, struct socket *sk); +void *transfergetdata(struct transfer *transfer, size_t *size); +void transferaddarg(struct transfer *transfer, wchar_t *rec, wchar_t *val); +int forkfilter(struct transfer *transfer); +void transferputdata(struct transfer *transfer, void *buf, size_t size); +size_t transferdatasize(struct transfer *transfer); +void transferendofdata(struct transfer *transfer); +void transferprepul(struct transfer *transfer, size_t size, size_t start, size_t end, struct socket *lesk); +void transferstartul(struct transfer *transfer, struct socket *sk); + +extern struct transfer *transfers; +EGCBCHAIN(newtransfercb, struct transfer *); + +#endif diff --git a/daemon/ui.c b/daemon/ui.c new file mode 100644 index 0000000..0302fa1 --- /dev/null +++ b/daemon/ui.c @@ -0,0 +1,2105 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "conf.h" +#include "auth.h" +#include "utils.h" +#include "net.h" +#include "module.h" +#include "sysevents.h" +#include "filenet.h" +#include "transfer.h" +#include "search.h" +#include "client.h" + +#define PERM_DISALLOW 1 +#define PERM_ADMIN 2 +#define PERM_FNETCTL 4 +#define PERM_TRANS 8 +#define PERM_TRANSCU 16 +#define PERM_CHAT 32 +#define PERM_SRCH 64 + +#define NOTIF_END 0 +#define NOTIF_INT 1 +#define NOTIF_STR 2 +#define NOTIF_FLOAT 3 +#define NOTIF_ID 4 +#define NOTIF_PEND 0 +#define NOTIF_WAIT 1 + +struct uidata; + +struct command +{ + wchar_t *name; + void (*handler)(struct socket *sk, struct uidata *data, int argc, wchar_t **argv); +}; + +struct qcommand +{ + struct qcommand *next; + struct command *cmd; + int argc; + wchar_t **argv; +}; + +struct uiuser +{ + struct uiuser *next, *prev; + int used; + wchar_t *name; + unsigned long perms; + int delete; +}; + +struct notif +{ + struct notif *next, *prev; + struct uidata *ui; + int state; + int code; + double rlimit; + size_t argc; + struct timer *exptimer; + struct notifarg + { + int dt; + union + { + int n; + wchar_t *s; + double d; + } d; + } *argv; +}; + +struct uidata +{ + struct uidata *next, *prev; + struct socket *sk; + struct qcommand *queue, *queuelast; + struct authhandle *auth; + int close; + union + { + struct + { + int fnact:1; + int fnchat:1; + int tract:1; + int trprog:1; + int srch:1; + } b; + int w; + } notify; + wchar_t *username; + struct uiuser *userinfo; + uid_t uid; + struct notif *fnotif, *lnotif; + char *fcmdbuf; + size_t fcmdbufdata, fcmdbufsize; + pid_t fcmdpid; + struct socket *fcmdsk; + /* Read buffer */ + char *inbuf; + size_t inbufsize, indata; + /* Wordset storage */ + wchar_t **argv; + int argc, args; + /* WCS conversation stuff */ + wchar_t *cb; /* Conversation buffer */ + size_t cbsize, cbdata; + iconv_t ichandle; + /* Parser data */ + int ps; /* Parser state */ + wchar_t *pp; /* Current parse pointer */ + wchar_t *cw; /* Current word (building storage) */ + size_t cwsize, cwdata; +}; + +static int srcheta(struct search *srch, void *uudata); +static int srchcommit(struct search *srch, void *uudata); +static int srchres(struct search *srch, struct srchres *sr, void *uudata); + +struct uiuser *users = NULL; +struct uidata *actives = NULL; +struct socket *uisocket = NULL; + +static wchar_t *quoteword(wchar_t *word) +{ + wchar_t *wp, *buf, *bp; + int dq, numbs, numc; + + dq = 0; + numbs = 0; + numc = 0; + if(*word == L'\0') + { + dq = 1; + } else { + for(wp = word; *wp != L'\0'; wp++) + { + if(!dq && iswspace(*wp)) + dq = 1; + if((*wp == L'\\') || (*wp == L'\"')) + numbs++; + numc++; + } + } + if(!dq && !numbs) + return(NULL); + bp = buf = smalloc(sizeof(wchar_t) * (numc + numbs + (dq?2:0) + 1)); + if(dq) + *(bp++) = L'\"'; + for(wp = word; *wp != L'\0'; wp++) + { + if((*wp == L'\\') || (*wp == L'\"')) + *(bp++) = L'\\'; + *(bp++) = *wp; + } + if(dq) + *(bp++) = L'\"'; + *(bp++) = L'\0'; + return(buf); +} + +static void sq(struct socket *sk, int cont, ...) +{ + int num, freepart; + va_list al; + char *final; + wchar_t *buf; + wchar_t *part, *tpart; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + num = 0; + va_start(al, cont); + while((part = va_arg(al, wchar_t *)) != NULL) + { + if(*part == L'%') + { + /* + * This kludge demands that all arguments that you call it + * with are the size of an int. That happens to be the + * case for most datatypes on most platforms and + * compilers, but I don't know exactly which ones, and + * also a long long is a notable candidate of an arg that + * is not the size of an int on 32-bit archs. If it breaks + * some existing call on your architecture, please tell + * me. + */ + part = vswprintf2(tpart = (part + 1), al); + for(; *tpart != L'\0'; tpart++) + { + if(*tpart == L'%') + { + if(tpart[1] == L'%') + tpart++; + else + va_arg(al, int); + } + } + freepart = 1; + } else { + freepart = 0; + } + if((tpart = quoteword(part)) != NULL) + { + if(freepart) + free(part); + part = tpart; + freepart = 1; + } + if((num > 1) || ((num == 1) && !(cont & 1))) + addtobuf(buf, L' '); + bufcat(buf, part, wcslen(part)); + if((num == 0) && (cont & 1)) + addtobuf(buf, L'-'); + num++; + if(freepart) + free(part); + } + if(cont & 2) + bufcat(buf, L" \0", 2); + else + bufcat(buf, L"\r\n\0", 3); + if((final = icwcstombs(buf, "utf-8")) == NULL) + { + flog(LOG_CRIT, "could not convert \"%ls\" into utf-8: %s", buf, strerror(errno)); + free(buf); + return; + } + va_end(al); + free(buf); + sockqueue(sk, final, strlen(final)); + free(final); +} + +struct uiuser *finduser(wchar_t *name) +{ + struct uiuser *user; + + for(user = users; user != NULL; user = user->next) + { + if(!wcscmp(user->name, name)) + break; + } + return(user); +} + +static void logout(struct uidata *data) +{ + data->userinfo = NULL; + if(data->username != NULL) + free(data->username); + data->username = NULL; + if(data->auth != NULL) + authputhandle(data->auth); + data->auth = NULL; +} + +static int haspriv(struct uidata *data, int perm) +{ + if(data->userinfo == NULL) + return(0); + if(data->userinfo->perms & perm) + return(1); + else + return(0); +} + +/* Useful macros for the command functions: */ +#define haveargs(n) do { if(argc < n) { sq(sk, 0, L"501", L"Wrong number of arguments", NULL); return; } } while(0) +#define havepriv(p) do { if((data->userinfo == NULL) || ((data->userinfo->perms & (p)) != (p))) { sq(sk, 0, L"502", L"%Unauthorized request - %x needed", (p), NULL); return; } } while(0) + +static void cmd_connect(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + int valid; + + if(confgetint("ui", "onlylocal")) + { + switch(sk->remote->sa_family) + { + case AF_INET: + valid = ((struct sockaddr_in *)sk->remote)->sin_addr.s_addr == INADDR_LOOPBACK; + break; + case AF_INET6: + valid = !memcmp(&((struct sockaddr_in6 *)sk->remote)->sin6_addr, &in6addr_loopback, sizeof(in6addr_loopback)); + break; + default: + valid = 0; + break; + } + if(!valid) + { + sq(sk, 0, L"502", L"Only localhost connections allowed to this host", NULL); + sk->close = 1; + data->close = 1; + return; + } + } + sq(sk, 0, L"200", L"%Dolda Connect daemon v%s", VERSION, NULL); +} + +static void cmd_notfound(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + if((argv != NULL) && (argv[0] != NULL)) + sq(sk, 0, L"500", L"%Command not found: %ls", argv[0], NULL); + else + sq(sk, 0, L"500", L"No command", NULL); +} + +static void cmd_shutdown(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + extern volatile int running; + + havepriv(PERM_ADMIN); + flog(LOG_NOTICE, "UI shutdown request from %ls, shutting down", data->username); + running = 0; + sq(sk, 0, L"200", L"Daemon shutting down", NULL); +} + +static void cmd_quit(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + sq(sk, 0, L"200", L"Closing connection", NULL); + data->close = 1; +} + +static void cmd_lsauth(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct authmech *mech, *prev; + + prev = NULL; + for(mech = mechs; mech != NULL; mech = mech->next) + { + if(mech->enabled) + { + if(prev != NULL) + sq(sk, 1, L"200", prev->name, NULL); + prev = mech; + } + } + if(prev == NULL) + sq(sk, 0, L"201", L"No authentication methods supported", NULL); + else + sq(sk, 0, L"200", prev->name, NULL); +} + +static void cmd_login(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + char *buf; + int code; + struct passwd *pwd; + + haveargs(3); + if(data->username != NULL) + { + if(data->userinfo != NULL) + sq(sk, 0, L"503", L"Already logged in", NULL); + else + sq(sk, 0, L"503", L"Already logging in", NULL); + return; + } + if((buf = icwcstombs(argv[2], NULL)) == NULL) + { + sq(sk, 0, L"504", L"Could not convert username to locale charset", NULL); + return; + } + data->username = swcsdup(argv[2]); + if((pwd = getpwnam(buf)) == NULL) + data->uid = -1; + else + data->uid = pwd->pw_uid; + if((data->auth = initauth(argv[1], buf)) == NULL) + { + if(errno == ENOENT) + sq(sk, 0, L"508", L"No such authentication mechanism", NULL); + else + sq(sk, 0, L"505", L"Could not initialize authentication system", L"%%s", strerror(errno), NULL); + free(buf); + logout(data); + return; + } + free(buf); + switch(authenticate(data->auth, NULL)) + { + case AUTH_SUCCESS: + data->userinfo = finduser(data->username); + if(data->userinfo == NULL) + data->userinfo = finduser(L"default"); + if(data->uid == -1) + { + sq(sk, 0, L"506", L"Authentication error", NULL); + flog(LOG_INFO, "user %ls authenticated successfully, but no account existed", data->username); + logout(data); + } else if((data->userinfo == NULL) || (data->userinfo->perms & PERM_DISALLOW)) { + sq(sk, 0, L"506", L"Authentication error", NULL); + flog(LOG_INFO, "user %ls authenticated successfully, but was not authorized", data->username); + logout(data); + } else { + sq(sk, 0, L"200", L"Welcome", NULL); + flog(LOG_INFO, "%ls (UID %i) logged in", data->username, data->uid); + } + break; + case AUTH_DENIED: + sq(sk, 0, L"506", L"Authentication error", L"%%ls", (data->auth->text == NULL)?L"":(data->auth->text), NULL); + logout(data); + break; + case AUTH_PASS: + switch(data->auth->prompt) + { + case AUTH_PR_AUTO: + code = 300; + break; + case AUTH_PR_NOECHO: + code = 301; + break; + case AUTH_PR_ECHO: + code = 302; + break; + case AUTH_PR_INFO: + code = 303; + break; + case AUTH_PR_ERROR: + code = 304; + break; + } + sq(sk, 0, L"%%i", code, data->auth->text, NULL); + break; + case AUTH_ERR: + sq(sk, 0, L"505", L"System error", L"%%s", strerror(errno), NULL); + logout(data); + break; + default: + flog(LOG_WARNING, "BUG? Non-caught return from authenticate in cmd_login"); + sq(sk, 0, L"505", L"System error", L"%%s", strerror(errno), NULL); + logout(data); + break; + } +} + +static void cmd_pass(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + char *buf; + int code; + + haveargs(2); + if((buf = icwcstombs(argv[1], NULL)) == NULL) + { + sq(sk, 0, L"504", L"Could not convert data to locale charset", NULL); + return; + } + if((data->auth == NULL) || (data->userinfo != NULL)) + { + sq(sk, 0, L"507", L"Data not expected", NULL); + return; + } + switch(authenticate(data->auth, buf)) + { + case AUTH_SUCCESS: + data->userinfo = finduser(data->username); + if(data->userinfo == NULL) + data->userinfo = finduser(L"default"); + if(data->uid == -1) + { + sq(sk, 0, L"506", L"Authentication error", NULL); + flog(LOG_INFO, "user %ls authenticated successfully, but no account existed", data->username); + logout(data); + } else if((data->userinfo == NULL) || (data->userinfo->perms & PERM_DISALLOW)) { + sq(sk, 0, L"506", L"Authentication error", NULL); + flog(LOG_INFO, "user %ls authenticated successfully, but was not authorized", data->username); + logout(data); + } else { + sq(sk, 0, L"200", L"Welcome", NULL); + flog(LOG_INFO, "%ls (UID %i) logged in", data->username, data->uid); + } + break; + case AUTH_DENIED: + sq(sk, 0, L"506", L"Authentication error", L"%%ls", (data->auth->text == NULL)?L"":(data->auth->text), NULL); + logout(data); + break; + case AUTH_PASS: + switch(data->auth->prompt) + { + case AUTH_PR_AUTO: + code = 300; + break; + case AUTH_PR_NOECHO: + code = 301; + break; + case AUTH_PR_ECHO: + code = 302; + break; + case AUTH_PR_INFO: + code = 303; + break; + case AUTH_PR_ERROR: + code = 304; + break; + } + sq(sk, 0, L"%%i", code, data->auth->text, NULL); + break; + case AUTH_ERR: + sq(sk, 0, L"505", L"System error", L"%%s", strerror(errno), NULL); + logout(data); + break; + default: + flog(LOG_WARNING, "BUG? Non-caught return from authenticate in cmd_pass"); + sq(sk, 0, L"505", L"System error", L"%%s", strerror(errno), NULL); + logout(data); + break; + } + free(buf); +} + +static void cmd_fnetconnect(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + int i; + char *buf; + int err; + struct fnetnode *fn; + + haveargs(3); + havepriv(PERM_FNETCTL); + if((buf = icwcstombs(argv[2], NULL)) == NULL) + { + sq(sk, 0, L"504", L"Could not convert data to locale charset", NULL); + return; + } + fn = fnetinitconnect(argv[1], buf); + err = errno; + free(buf); + if(fn == NULL) + { + if(errno == EPROTONOSUPPORT) + sq(sk, 0, L"511", L"No such network name", NULL); + else + sq(sk, 0, L"509", L"Could not parse the address", L"%%s", strerror(err), NULL); + return; + } + for(i = 3; i < argc - 1; i += 2) + { + if(!wcscmp(argv[i], L"nick")) + fnetsetnick(fn, argv[i + 1]); + } + linkfnetnode(fn); + fnetsetname(fn, argv[2]); + putfnetnode(fn); + sq(sk, 0, L"200", L"Connection under way", NULL); +} + +static void cmd_lsnodes(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct fnetnode *fn; + + if(fnetnodes == NULL) + { + sq(sk, 0, L"201", L"No connected nodes", NULL); + return; + } + for(fn = fnetnodes; fn != NULL; fn = fn->next) + { + sq(sk, (fn->next != NULL)?1:0, L"200", L"%%i", fn->id, fn->fnet->name, (fn->name == NULL)?L"":fn->name, L"%%i", fn->numpeers, L"%%i", fn->state, NULL); + } +} + +static void cmd_disconnect(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct fnetnode *fn; + int i; + + haveargs(2); + havepriv(PERM_FNETCTL); + /* Note - Programmatical user interfaces must only give one + * argument per command, the multiple argument form is only for + * convenience when manually controlling the daemon via + * eg. telnet. The reason is that the return codes aren't clear + * enough for the multiple argument form. */ + for(i = 1; i < argc; i++) + { + if((fn = findfnetnode(wcstol(argv[i], NULL, 0))) == NULL) + { + sq(sk, 0, L"510", L"No such node", NULL); + return; + } + killfnetnode(fn); + unlinkfnetnode(fn); + } + sq(sk, 0, L"200", L"Node flagged for disconnection", NULL); +} + +static void cmd_lspa(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct fnetnode *fn; + struct fnetpeerdatum *datum; + + haveargs(2); + if((fn = findfnetnode(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"510", L"No such node", NULL); + return; + } + if(fn->peerdata == NULL) + { + sq(sk, 0, L"201", L"No data available", NULL); + } else { + for(datum = fn->peerdata; datum != NULL; datum = datum->next) + sq(sk, (datum->next != NULL)?1:0, L"200", datum->id, L"%%i", datum->datatype, NULL); + } +} + +static void cmd_lspeers(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + int i; + struct fnetnode *fn; + struct fnetpeer *peer; + wchar_t buf[40]; + + haveargs(2); + if((fn = findfnetnode(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"510", L"No such node", NULL); + return; + } + if(fn->peers == NULL) + { + sq(sk, 0, L"201", L"No peers avaiable", NULL); + } else { + for(peer = fn->peers; peer != NULL; peer = peer->next) + { + sq(sk, 2 | ((peer->next != NULL)?1:0), L"200", peer->id, peer->nick, NULL); + for(i = 0; i < peer->dinum; i++) + { + if(peer->peerdi[i].datum->datatype == FNPD_INT) + sq(sk, 2, peer->peerdi[i].datum->id, L"%%i", peer->peerdi[i].data.num, NULL); + /* Note: A long long is not the size of an int, so + * sq() can't handle the conversion itself. */ + if(peer->peerdi[i].datum->datatype == FNPD_LL) + { + swprintf(buf, 40, L"%lli", peer->peerdi[i].data.lnum); + sq(sk, 2, peer->peerdi[i].datum->id, buf, NULL); + } + if((peer->peerdi[i].datum->datatype == FNPD_STR) && (peer->peerdi[i].data.str != NULL)) + sq(sk, 2, peer->peerdi[i].datum->id, peer->peerdi[i].data.str, NULL); + } + sq(sk, 0, NULL); + } + } +} + +static void cmd_download(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + int i; + struct fnet *net; + struct fnetnode *fn; + struct transfer *transfer; + struct fnetpeer *peer; + + haveargs(4); + if((argc > 5) && ((argc % 2) == 0)) + { + sq(sk, 0, L"501", L"Must have an even number of arguments", NULL); + return; + } + havepriv(PERM_TRANS); + if((*(argv[1]) >= L'0') && (*(argv[1]) <= L'9')) + { + if((fn = findfnetnode(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"510", L"No such node", NULL); + return; + } + net = fn->fnet; + } else { + fn = NULL; + if((net = findfnet(argv[1])) == NULL) + { + sq(sk, 0, L"511", L"No such network name", NULL); + return; + } + } + transfer = newtransfer(); + authgethandle(transfer->auth = data->auth); + transfer->fnet = net; + transfer->peerid = swcsdup(argv[2]); + transfer->path = swcsdup(argv[3]); + transfer->dir = TRNSD_DOWN; + transfer->owner = data->uid; + if(fn != NULL) + { + transfer->fn = fn; + getfnetnode(fn); + linktransfer(transfer); + if(((peer = fnetfindpeer(fn, transfer->peerid)) != NULL) && (peer->nick != NULL)) + transfersetnick(transfer, peer->nick); + } else { + linktransfer(transfer); + } + if(argc > 4) + transfersetsize(transfer, wcstol(argv[4], NULL, 0)); + if(argc > 5) + { + for(i = 5; i < argc; i += 2) + transferaddarg(transfer, argv[i], argv[i + 1]); + } + sq(sk, 0, L"200", L"%%i", transfer->id, L"Download queued", NULL); + transfersetactivity(transfer, L"create"); +} + +static void cmd_lstrans(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct transfer *transfer, *pt; + + havepriv(PERM_TRANS); + pt = NULL; + for(transfer = transfers; transfer != NULL; transfer = transfer->next) + { + if((transfer->dir != TRNSD_DOWN) || (transfer->owner == data->uid)) + { + if(pt != NULL) + sq(sk, 1, L"200", L"%%i", pt->id, L"%%i", pt->dir, + L"%%i", pt->state, pt->peerid, + (pt->peernick == NULL)?L"":(pt->peernick), + (pt->path == NULL)?L"":(pt->path), + L"%%i", pt->size, L"%%i", pt->curpos, + NULL); + pt = transfer; + } + } + if(pt == NULL) + sq(sk, 0, L"201", L"No transfers", NULL); + else + sq(sk, 0, L"200", L"%%i", pt->id, L"%%i", pt->dir, + L"%%i", pt->state, pt->peerid, + (pt->peernick == NULL)?L"":(pt->peernick), + (pt->path == NULL)?L"":(pt->path), + L"%%i", pt->size, L"%%i", pt->curpos, + NULL); +} + +static void cmd_cancel(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct transfer *transfer; + + haveargs(2); + havepriv(PERM_TRANS); + if((transfer = findtransfer(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"512", L"No such transfer", NULL); + return; + } + if((transfer->dir == TRNSD_UP) && !(data->userinfo->perms & PERM_TRANSCU)) + { + sq(sk, 0, L"502", L"You are not allowed to cancel uploads", NULL); + return; + } + if((transfer->dir == TRNSD_DOWN) && (transfer->owner != data->uid)) + { + sq(sk, 0, L"502", L"You do not own that transfer", NULL); + return; + } + transfer->close = 1; + sq(sk, 0, L"200", L"Transfer cancelled", NULL); +} + +static void cmd_notify(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + int i, val; + + if((argc % 2) != 1) + { + sq(sk, 0, L"501", L"Must have an even number of arguments", NULL); + return; + } + for(i = 1; i < argc; i += 2) + { + if(!wcscasecmp(argv[i + 1], L"on")) + val = 1; + else + val = 0; + if(!wcscasecmp(argv[i], L"all")) + { + if(val) + data->notify.w = ~0; + else + data->notify.w = 0; + } else if(!wcscasecmp(argv[i], L"fn:chat")) { + data->notify.b.fnchat = val; + } else if(!wcscasecmp(argv[i], L"fn:act")) { + data->notify.b.fnact = val; + } else if(!wcscasecmp(argv[i], L"trans:act")) { + data->notify.b.tract = val; + } else if(!wcscasecmp(argv[i], L"trans:prog")) { + data->notify.b.trprog = val; + } else if(!wcscasecmp(argv[i], L"srch:act")) { + data->notify.b.srch = val; + } + } + sq(sk, 0, L"200", L"Notification alteration succeeded", NULL); +} + +static void cmd_sendchat(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct fnetnode *fn; + int public; + + haveargs(5); + havepriv(PERM_CHAT); + if((fn = findfnetnode(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"510", L"No such node", NULL); + return; + } + public = wcstol(argv[2], NULL, 0); + if((public != 0) && (public != 1)) + { + sq(sk, 0, L"509", L"Second argument must be 0 or 1", NULL); + return; + } + if(fn->state != FNN_EST) + { + sq(sk, 0, L"513", L"Hub is in state FNN_EST", NULL); + return; + } + if(fnetsendchat(fn, public, argv[3], argv[4])) + { + if(errno == ENOTSUP) + sq(sk, 0, L"513", L"This network does not support chatting", NULL); + else if(errno == EPERM) + sq(sk, 0, L"502", L"This node does not allow you to chat", NULL); + else if(errno == EILSEQ) + sq(sk, 0, L"504", L"This network could not support all the characters in that message", NULL); + else + sq(sk, 0, L"505", L"Could not chat", NULL); + return; + } + sq(sk, 0, L"200", L"Chat string sent", NULL); +} + +static void cmd_search(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct search *srch; + struct fnetnode *fn; + struct sexpr *sexpr; + int i; + + haveargs(3); + havepriv(PERM_SRCH); + srch = newsearch(data->username, NULL); + for(i = 1; i < argc; i++) + { + if(!wcscmp(argv[i], L"all")) + { + for(fn = fnetnodes; fn != NULL; fn = fn->next) + { + if(fn->state == FNN_EST) + searchaddfn(srch, fn); + } + i++; + break; + } else if(!wcscmp(argv[i], L"prio")) { + if(++i == argc) + { + sq(sk, 0, L"501", L"No argument to prio", NULL); + freesearch(srch); + return; + } + srch->prio = wcstol(argv[i], NULL, 0); + } else if(iswdigit(*argv[i])) { + if((fn = findfnetnode(wcstol(argv[i], NULL, 0))) == NULL) + { + sq(sk, 0, L"510", L"No such node", NULL); + freesearch(srch); + return; + } + searchaddfn(srch, fn); + } else { + break; + } + } + if(srch->fnl == NULL) + { + sq(sk, 0, L"501", L"No fnetnodes to search found on line", NULL); + freesearch(srch); + return; + } + if(i == argc) + { + sq(sk, 0, L"501", L"No search expression found on line", NULL); + freesearch(srch); + return; + } + if((sexpr = parsesexpr(argc - i, argv + i)) == NULL) + { + sq(sk, 0, L"509", L"Could not parse search expression", NULL); + freesearch(srch); + return; + } + optsexpr(sexpr); + getsexpr(srch->sexpr = sexpr); + queuesearch(srch); + CBREG(srch, search_eta, srcheta, NULL, NULL); + CBREG(srch, search_commit, srchcommit, NULL, NULL); + CBREG(srch, search_result, srchres, NULL, NULL); + sq(sk, 0, L"200", L"%%i", srch->id, L"%%i", srch->eta - time(NULL), NULL); + putsexpr(sexpr); +} + +static void cmd_lssrch(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct search *srch, *pt; + time_t now; + + havepriv(PERM_SRCH); + pt = NULL; + now = time(NULL); + for(srch = searches; srch != NULL; srch = srch->next) + { + if(!wcscmp(srch->owner, data->username)) + { + if(pt != NULL) + sq(sk, 1, L"200", L"%%i", pt->id, L"%%i", pt->state, L"%%i", pt->eta - now, L"%%i", pt->numres, NULL); + pt = srch; + } + } + if(pt == NULL) + sq(sk, 0, L"201", L"No searches", NULL); + else + sq(sk, 0, L"200", L"%%i", pt->id, L"%%i", pt->state, L"%%i", pt->eta - now, L"%%i", pt->numres, NULL); +} + +static void cmd_lssr(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct search *srch; + struct srchres *sr; + wchar_t buf[64]; + + haveargs(2); + havepriv(PERM_SRCH); + if((srch = findsearch(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"514", L"No such search", NULL); + return; + } + if(srch->results == NULL) + { + sq(sk, 0, L"201", L"No results", NULL); + } else { + for(sr = srch->results; sr != NULL; sr = sr->next) + { + swprintf(buf, 64, L"%f", sr->time); + sq(sk, (sr->next != NULL)?1:0, L"200", L"%%ls", sr->filename, sr->fnet->name, L"%%ls", sr->peerid, L"%%i", sr->size, L"%%i", sr->slots, L"%%i", (sr->fn == NULL)?-1:(sr->fn->id), buf, NULL); + } + } +} + +static void cmd_cansrch(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct search *srch; + int i; + + haveargs(2); + havepriv(PERM_SRCH); + /* Note - Programmatical user interfaces must only give one + * argument per command, the multiple argument form is only for + * convenience when manually controlling the daemon via + * eg. telnet. The reason is that the return codes aren't clear + * enough for the multiple argument form. */ + for(i = 1; i < argc; i++) + { + if((srch = findsearch(wcstol(argv[i], NULL, 0))) == NULL) + { + sq(sk, 0, L"514", L"No such search", NULL); + return; + } + freesearch(srch); + } + sq(sk, 0, L"200", L"Search cancelled", NULL); +} + +static void fcmdread(struct socket *sk, struct uidata *data) +{ + char *buf; + size_t bufsize; + + if((buf = sockgetinbuf(sk, &bufsize)) == NULL) + return; + bufcat(data->fcmdbuf, buf, bufsize); + free(buf); +} + +static void fcmderr(struct socket *sk, int err, struct uidata *data) +{ + wchar_t *wbuf, *p, *p2; + + if(err) + { + flog(LOG_WARNING, "error occurred on filtercmd pipe socket: %s", strerror(err)); + kill(-data->fcmdpid, SIGHUP); + putsock(data->fcmdsk); + data->fcmdsk = NULL; + if(data->fcmdbuf != NULL) + { + free(data->fcmdbuf); + data->fcmdbuf = NULL; + } + data->fcmdbufsize = data->fcmdbufdata = 0; + sq(data->sk, 0, L"505", L"An error occurred on the pipe to the filtercmd", L"%%s", strerror(err), NULL); + return; + } + putsock(data->fcmdsk); + data->fcmdsk = NULL; + data->fcmdpid = 0; + if(data->fcmdbuf == NULL) + { + wbuf = swcsdup(L""); + } else { + addtobuf(data->fcmdbuf, 0); + wbuf = icmbstowcs(data->fcmdbuf, NULL); + free(data->fcmdbuf); + } + data->fcmdbuf = NULL; + data->fcmdbufsize = data->fcmdbufdata = 0; + if(wbuf == NULL) + { + sq(data->sk, 0, L"504", L"Filtercmd sent data which could not be converted from the local charset", NULL); + return; + } + p = wbuf; + for(p2 = wcschr(p, L'\n'); p2 != NULL; p2 = wcschr(p, L'\n')) + { + *(p2++) = L'\0'; + sq(data->sk, (*p2 == L'\0')?0:1, L"200", L"%%ls", p, NULL); + p = p2; + } + if(*p == L'\0') + { + if(p == wbuf) + sq(data->sk, 0, L"201", L"No data returned", NULL); + } else { + sq(data->sk, 0, L"200", L"%%ls", p, NULL); + } + free(wbuf); +} + +static void cmd_filtercmd(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + int i; + pid_t pid; + int pipe; + char **cargv, **pp; + char *filtercmd, *argbuf; + size_t cargvsize, cargvdata; + struct passwd *pwent; + + haveargs(2); + havepriv(PERM_TRANS); + if((pwent = getpwuid(data->uid)) == NULL) + { + flog(LOG_WARNING, "no passwd entry for UI user %i", data->uid); + sq(sk, 0, L"505", L"System error - Could not fork session", "Internal error", NULL); + return; + } + if((filtercmd = findfile(icswcstombs(confgetstr("ui", "filtercmd"), NULL, NULL), "dcdl-filtercmd", pwent->pw_dir)) == NULL) + { + flog(LOG_WARNING, "could not find filtercmd executable for user %s", pwent->pw_name); + sq(sk, 0, L"505", L"System error - Could not fork session", L"Could not find filtercmd executable", NULL); + return; + } + cargv = NULL; + cargvsize = cargvdata = 0; + addtobuf(cargv, filtercmd); + for(i = 1; i < argc; i++) + { + if((argbuf = icwcstombs(argv[i], NULL)) == NULL) + { + for(i = 0; i < cargvdata; i++) + free(cargv[i]); + free(cargv); + sq(sk, 0, L"504", L"%Could not convert argument %i into local character set", i, L"%%s", strerror(errno), NULL); + return; + } + addtobuf(cargv, argbuf); + } + addtobuf(cargv, NULL); + if((pid = forksess(data->uid, data->auth, NULL, NULL, FD_FILE, 0, O_RDWR, "/dev/null", FD_PIPE, 1, O_RDONLY, &pipe, FD_FILE, 2, O_RDWR, "/dev/null", FD_END)) < 0) + { + flog(LOG_WARNING, "could not fork session in filtercmd: %s", strerror(errno)); + sq(sk, 0, L"505", L"System error - Could not fork session", L"%%s", strerror(errno), NULL); + return; + } + if(pid == 0) + { + execv(filtercmd, cargv); + flog(LOG_WARNING, "could not exec filtercmd %s: %s", filtercmd, strerror(errno)); + exit(127); + } + for(pp = cargv; *pp; pp++) + free(*pp); + free(cargv); + data->fcmdsk = wrapsock(pipe); + data->fcmdpid = pid; + if(data->fcmdbuf != NULL) + { + free(data->fcmdbuf); + data->fcmdbuf = NULL; + } + data->fcmdbufsize = data->fcmdbufdata = 0; + data->fcmdsk->data = data; + data->fcmdsk->readcb = (void (*)(struct socket *, void *))fcmdread; + data->fcmdsk->errcb = (void (*)(struct socket *, int, void *))fcmderr; +} + +static void cmd_lstrarg(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct transfer *transfer; + struct transarg *ta; + + haveargs(2); + havepriv(PERM_TRANS); + if((transfer = findtransfer(wcstol(argv[1], NULL, 0))) == NULL) + { + sq(sk, 0, L"512", L"No such transfer", NULL); + return; + } + if((transfer->dir == TRNSD_DOWN) && (transfer->owner != data->uid)) + { + sq(sk, 0, L"502", L"You do not own that transfer", NULL); + return; + } + if(transfer->args == NULL) + { + sq(sk, 0, L"201", L"Transfer has no arguments", NULL); + } else { + for(ta = transfer->args; ta != NULL; ta = ta->next) + sq(sk, ta->next != NULL, L"200", L"%%ls", ta->rec, L"%%ls", ta->val, NULL); + } +} + +static void cmd_hashstatus(struct socket *sk, struct uidata *data, int argc, wchar_t **argv) +{ + struct sharecache *node; + int total, hashed; + + total = hashed = 0; + for(node = shareroot->child; node != NULL; node = nextscnode(node)) + { + if(node->f.b.type == FILE_REG) + { + total++; + if(node->f.b.hastth) + hashed++; + } + } + sq(sk, 0, L"200", L"%%i", total, L"tth", L"%%i", hashed, NULL); +} + +#undef haveargs +#undef havepriv + +/* + * Reserved command numbers for nameless commands: + * 0: Issued when a client has connected + * 1: Issued when a named command couldn't be found + */ + +static struct command commands[] = +{ + {NULL, cmd_connect}, + {NULL, cmd_notfound}, + {L"shutdown", cmd_shutdown}, + {L"quit", cmd_quit}, + {L"lsauth", cmd_lsauth}, + {L"login", cmd_login}, + {L"pass", cmd_pass}, + {L"cnct", cmd_fnetconnect}, + {L"lsnodes", cmd_lsnodes}, + {L"dcnct", cmd_disconnect}, + {L"lspa", cmd_lspa}, + {L"lspeers", cmd_lspeers}, + {L"download", cmd_download}, + {L"lstrans", cmd_lstrans}, + {L"cancel", cmd_cancel}, + {L"notify", cmd_notify}, + {L"sendchat", cmd_sendchat}, + {L"search", cmd_search}, + {L"lssrch", cmd_lssrch}, + {L"lssr", cmd_lssr}, + {L"cansrch", cmd_cansrch}, + {L"filtercmd", cmd_filtercmd}, + {L"lstrarg", cmd_lstrarg}, + {L"hashstatus", cmd_hashstatus}, + {NULL, NULL} +}; + +static void freequeuecmd(struct qcommand *qcmd) +{ + int i; + + if(qcmd->argv != NULL) + { + for(i = 0; i < qcmd->argc; i++) + free(qcmd->argv[i]); + free(qcmd->argv); + } + free(qcmd); +} + +static struct qcommand *unlinkqcmd(struct uidata *data) +{ + struct qcommand *qcmd; + + qcmd = data->queue; + if(qcmd != NULL) + { + data->queue = qcmd->next; + if(qcmd == data->queuelast) + data->queuelast = qcmd->next; + } + return(qcmd); +} + +static struct notif *newnotif(struct uidata *data, int code, ...) +{ + struct notif *notif; + va_list args; + int dt, ca; + + notif = smalloc(sizeof(*notif)); + memset(notif, 0, sizeof(*notif)); + notif->rlimit = 0.0; + notif->ui = data; + notif->code = code; + va_start(args, code); + while((dt = va_arg(args, int)) != NOTIF_END) + { + ca = notif->argc; + notif->argv = realloc(notif->argv, sizeof(*notif->argv) * ++notif->argc); + notif->argv[ca].dt = dt; + switch(dt) + { + case NOTIF_INT: + case NOTIF_ID: + notif->argv[ca].d.n = va_arg(args, int); + break; + case NOTIF_STR: + notif->argv[ca].d.s = wcsdup(va_arg(args, wchar_t *)); + break; + case NOTIF_FLOAT: + notif->argv[ca].d.d = va_arg(args, double); + break; + } + } + va_end(args); + notif->next = NULL; + notif->prev = data->lnotif; + if(data->lnotif != NULL) + data->lnotif->next = notif; + else + data->fnotif = notif; + data->lnotif = notif; + return(notif); +} + +static void freenotif(struct notif *notif) +{ + int i; + + if(notif->next != NULL) + notif->next->prev = notif->prev; + if(notif->prev != NULL) + notif->prev->next = notif->next; + if(notif == notif->ui->fnotif) + notif->ui->fnotif = notif->next; + if(notif == notif->ui->lnotif) + notif->ui->lnotif = notif->prev; + if(notif->exptimer != NULL) + canceltimer(notif->exptimer); + for(i = 0; i < notif->argc; i++) + { + if(notif->argv[i].dt == NOTIF_STR) + free(notif->argv[i].d.s); + } + if(notif->argv != NULL) + free(notif->argv); + free(notif); +} + +static void notifexpire(int cancelled, struct notif *notif) +{ + notif->exptimer = NULL; + if(!cancelled) + freenotif(notif); +} + +static struct notif *findnotif(struct notif *notif, int dir, int state, int code, int id) +{ + int i, cont; + + for(; notif != NULL; notif = (dir?notif->next:notif->prev)) + { + if((notif->code == code) && ((state < 0) || (state == notif->state))) + { + cont = 0; + if(id >= 0) + { + for(i = 0; i < notif->argc; i++) + { + if((notif->argv[i].dt == NOTIF_ID) && (notif->argv[i].d.n != id)) + { + cont = 1; + break; + } + } + } + if(cont) + continue; + break; + } + } + return(notif); +} + +static void freeuidata(struct uidata *data) +{ + int i; + struct qcommand *qcmd; + + if(data->next != NULL) + data->next->prev = data->prev; + if(data->prev != NULL) + data->prev->next = data->next; + if(data == actives) + actives = data->next; + data->sk->readcb = NULL; + data->sk->errcb = NULL; + putsock(data->sk); + while((qcmd = unlinkqcmd(data)) != NULL) + freequeuecmd(qcmd); + iconv_close(data->ichandle); + if(data->cw != NULL) + free(data->cw); + if(data->cb != NULL) + free(data->cb); + if(data->argv != NULL) + { + for(i = 0; i < data->argc; i++) + free(data->argv[i]); + free(data->argv); + } + if(data->auth != NULL) + authputhandle(data->auth); + if(data->username != NULL) + { + if(data->userinfo != NULL) + flog(LOG_INFO, "%ls logged out", data->username); + free(data->username); + } + free(data->inbuf); + while(data->fnotif != NULL) + freenotif(data->fnotif); + if(data->fcmdbuf != NULL) + free(data->fcmdbuf); + if(data->fcmdpid != 0) + kill(-data->fcmdpid, SIGHUP); + if(data->fcmdsk != NULL) + putsock(data->fcmdsk); + free(data); +} + +static void queuecmd(struct uidata *data, struct command *cmd, int argc, wchar_t **argv) +{ + struct qcommand *new; + + new = smalloc(sizeof(*new)); + new->cmd = cmd; + new->argc = argc; + new->argv = argv; + new->next = NULL; + if(data->queuelast != NULL) + data->queuelast->next = new; + data->queuelast = new; + if(data->queue == NULL) + data->queue = new; +} + +static struct uidata *newuidata(struct socket *sk) +{ + struct uidata *data; + + data = smalloc(sizeof(*data)); + memset(data, 0, sizeof(*data)); + data->sk = sk; + getsock(sk); + data->inbuf = smalloc(1024); + data->uid = -1; + if((data->ichandle = iconv_open("wchar_t", "utf-8")) == (iconv_t)-1) + { + flog(LOG_CRIT, "iconv cannot handle UTF-8: %s", strerror(errno)); + return(NULL); + } + data->next = actives; + data->prev = NULL; + if(actives != NULL) + actives->prev = data; + actives = data; + return(data); +} + +static void uiread(struct socket *sk, struct uidata *data) +{ + int ret, done; + char *newbuf; + char *p1, *p2; + wchar_t *porig; + size_t datalen, len2; + struct command *cur; + + if(data->indata > 1024) + data->indata = 0; + if((newbuf = sockgetinbuf(sk, &datalen)) == NULL) + return; + sizebuf(&data->inbuf, &data->inbufsize, data->indata + datalen, 1, 1); + memcpy(data->inbuf + data->indata, newbuf, datalen); + free(newbuf); + data->indata += datalen; + if(data->cb == NULL) + { + data->cb = smalloc(sizeof(wchar_t) * (data->cbsize = 64)); + data->cbdata = 0; + data->pp = data->cb; + } + done = 0; + while(!done) + { + if(data->cbsize == data->cbdata) + { + len2 = data->pp - data->cb; + data->cb = srealloc(data->cb, sizeof(wchar_t) * (data->cbsize *= 2)); + data->pp = data->cb + len2; + } + p1 = data->inbuf; + p2 = (char *)(porig = (data->cb + data->cbdata)); + len2 = sizeof(wchar_t) * (data->cbsize - data->cbdata); + ret = iconv(data->ichandle, &p1, &data->indata, &p2, &len2); + memmove(data->inbuf, p1, data->indata); + /* Just a sanity check */ + if(((p2 - ((char *)data->cb)) % sizeof(wchar_t)) != 0) + { + flog(LOG_CRIT, "Aiya! iconv does strange things to our wchar_t's!"); + abort(); + } + data->cbdata += (((wchar_t *)p2) - porig); + if(ret < 0) + { + switch(errno) + { + case EILSEQ: + /* XXX: Should this really just ignore it? */ + data->indata = 0; + done = 1; + break; + case EINVAL: + done = 1; + break; + case E2BIG: + /* Just a sanity check */ + if(data->cbsize != data->cbdata) + { + flog(LOG_CRIT, "Aiya! iconv doesn't give us wchar_t's!"); + abort(); + } + break; + default: + flog(LOG_WARNING, "bug: strange error from iconv in uiread: %s", strerror(errno)); + break; + } + } else { + done = 1; + } + } + done = 0; + while(!done && (data->pp - data->cb < data->cbdata)) + { + switch(data->ps) + { + case 0: + if(iswspace(*data->pp)) + { + if(*data->pp == L'\r') + { + if(data->pp == data->cb + data->cbdata - 1) + { + done = 1; + break; + } + if(*(++data->pp) == L'\n') + { + if((data->argv != NULL) && (data->argv[0] != NULL)) + { + for(cur = commands; cur->handler != NULL; cur++) + { + if(cur->name == NULL) + continue; + if(!wcscasecmp(cur->name, data->argv[0])) + { + queuecmd(data, cur, data->argc, data->argv); + break; + } + } + if(cur->handler == NULL) + queuecmd(data, &commands[1], data->argc, data->argv); + } else { + queuecmd(data, &commands[1], data->argc, data->argv); + } + data->argv = NULL; + data->args = 0; + data->argc = 0; + wmemmove(data->cb, data->pp, data->cbdata -= (data->pp - data->cb)); + data->pp = data->cb; + } else { + data->pp++; + } + } else { + data->pp++; + } + } else { + data->ps = 1; + data->cwdata = 0; + } + break; + case 1: + if(iswspace(*data->pp)) + { + addtobuf(data->cw, L'\0'); + sizebuf(&data->argv, &data->args, data->argc + 1, sizeof(*data->argv), 1); + data->argv[data->argc++] = data->cw; + data->cw = NULL; + data->cwsize = 0; + data->cwdata = 0; + data->ps = 0; + } else if(*data->pp == L'\"') { + data->ps = 2; + data->pp++; + } else if(*data->pp == L'\\') { + if(data->pp == data->cb + data->cbdata - 1) + { + done = 1; + break; + } + addtobuf(data->cw, *(++data->pp)); + data->pp++; + } else { + addtobuf(data->cw, *(data->pp++)); + } + break; + case 2: + if(*data->pp == L'\"') + { + data->ps = 1; + } else if(*data->pp == L'\\') { + if(data->pp == data->cb + data->cbdata - 1) + { + done = 1; + break; + } + addtobuf(data->cw, *(++(data->pp))); + } else { + addtobuf(data->cw, *data->pp); + } + data->pp++; + break; + } + } +} + +static void uierror(struct socket *sk, int err, struct uidata *data) +{ + if(err) + flog(LOG_WARNING, "error occurred on UI socket: %s", strerror(err)); + freeuidata(data); +} + +static void uiaccept(struct socket *sk, struct socket *newsk, void *data) +{ + struct uidata *uidata; + + newsk->data = uidata = newuidata(newsk); + socksettos(newsk, confgetint("ui", "uitos")); + if(uidata == NULL) + return; + newsk->errcb = (void (*)(struct socket *, int, void *))uierror; + newsk->readcb = (void (*)(struct socket *, void *))uiread; + queuecmd(uidata, &commands[0], 0, NULL); +} + +static int srcheta(struct search *srch, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_SRCH) && data->notify.b.srch && !wcscmp(srch->owner, data->username)) + newnotif(data, 620, NOTIF_ID, srch->id, NOTIF_INT, srch->eta - time(NULL), NOTIF_END); + } + return(0); +} + +static int srchcommit(struct search *srch, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_SRCH) && data->notify.b.srch && !wcscmp(srch->owner, data->username)) + newnotif(data, 621, NOTIF_ID, srch->id, NOTIF_END); + } + return(0); +} + +static int srchres(struct search *srch, struct srchres *sr, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_SRCH) && data->notify.b.srch && !wcscmp(srch->owner, data->username)) + newnotif(data, 622, NOTIF_ID, srch->id, NOTIF_STR, sr->filename, NOTIF_STR, sr->fnet->name, NOTIF_STR, sr->peerid, NOTIF_INT, sr->size, NOTIF_INT, sr->slots, NOTIF_INT, (sr->fn == NULL)?-1:(sr->fn->id), NOTIF_FLOAT, sr->time, NOTIF_END); + } + return(0); +} + +static int recvchat(struct fnetnode *fn, int public, wchar_t *name, wchar_t *peer, wchar_t *string, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_CHAT) && data->notify.b.fnchat) + newnotif(data, 600, NOTIF_ID, fn->id, NOTIF_INT, public, NOTIF_STR, name, NOTIF_STR, peer, NOTIF_STR, string, NOTIF_END); + } + return(0); +} + +static int fnactive(struct fnetnode *fn, wchar_t *attrib, void *uudata) +{ + struct uidata *data; + struct notif *notif; + + if(!wcscmp(attrib, L"state")) + { + for(data = actives; data != NULL; data = data->next) + { + if(data->notify.b.fnact) + newnotif(data, 601, NOTIF_ID, fn->id, NOTIF_INT, fn->state, NOTIF_END); + } + } else if(!wcscmp(attrib, L"name")) { + for(data = actives; data != NULL; data = data->next) + { + if(data->notify.b.fnact) + newnotif(data, 602, NOTIF_ID, fn->id, NOTIF_STR, fn->name, NOTIF_END); + } + } else if(!wcscmp(attrib, L"numpeers")) { + for(data = actives; data != NULL; data = data->next) + { + if(data->notify.b.fnact) + { + if((notif = findnotif(data->fnotif, 1, NOTIF_PEND, 605, fn->id)) != NULL) + notif->argv[1].d.n = fn->numpeers; + else + newnotif(data, 605, NOTIF_ID, fn->id, NOTIF_INT, fn->numpeers, NOTIF_END); + } + } + } + return(0); +} + +static int fnunlink(struct fnetnode *fn, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(data->notify.b.fnact) + newnotif(data, 603, NOTIF_ID, fn->id, NOTIF_END); + } + return(0); +} + +static int newfnetnode(struct fnetnode *fn, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(data->notify.b.fnact) + newnotif(data, 604, NOTIF_ID, fn->id, NOTIF_STR, fn->fnet->name, NOTIF_END); + } + CBREG(fn, fnetnode_ac, fnactive, NULL, NULL); + CBREG(fn, fnetnode_chat, recvchat, NULL, NULL); + CBREG(fn, fnetnode_unlink, fnunlink, NULL, NULL); + return(0); +} + +static int transferchattr(struct transfer *transfer, wchar_t *attrib, void *uudata) +{ + struct uidata *data; + + if(!wcscmp(attrib, L"state")) + { + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 611, NOTIF_ID, transfer->id, NOTIF_INT, transfer->state, NOTIF_END); + } + } else if(!wcscmp(attrib, L"nick")) { + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 612, NOTIF_ID, transfer->id, NOTIF_STR, transfer->peernick, NOTIF_END); + } + } else if(!wcscmp(attrib, L"size")) { + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 613, NOTIF_ID, transfer->id, NOTIF_INT, transfer->size, NOTIF_END); + } + } else if(!wcscmp(attrib, L"error")) { + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 614, NOTIF_ID, transfer->id, NOTIF_INT, transfer->error, NOTIF_END); + } + } else if(!wcscmp(attrib, L"path")) { + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 616, NOTIF_ID, transfer->id, NOTIF_STR, transfer->path, NOTIF_END); + } + } + return(0); +} + +static int transferprog(struct transfer *transfer, void *uudata) +{ + struct uidata *data; + struct notif *notif; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.trprog && ((transfer->owner == 0) || (transfer->owner == data->uid))) + { + if((notif = findnotif(data->fnotif, 1, NOTIF_PEND, 615, transfer->id)) != NULL) + notif->argv[1].d.n = transfer->curpos; + else + newnotif(data, 615, NOTIF_ID, transfer->id, NOTIF_INT, transfer->curpos, NOTIF_END)->rlimit = 0.5; + } + } + return(0); +} + +static int transferdestroyed(struct transfer *transfer, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 617, NOTIF_ID, transfer->id, NOTIF_END); + } + return(0); +} + +static int newtransfernotify(struct transfer *transfer, void *uudata) +{ + struct uidata *data; + + for(data = actives; data != NULL; data = data->next) + { + if(haspriv(data, PERM_TRANS) && data->notify.b.tract && ((transfer->owner == 0) || (transfer->owner == data->uid))) + newnotif(data, 610, NOTIF_ID, transfer->id, NOTIF_INT, transfer->dir, NOTIF_STR, transfer->peerid, NOTIF_STR, (transfer->path == NULL)?L"":transfer->path, NOTIF_END); + } + CBREG(transfer, trans_ac, transferchattr, NULL, NULL); + CBREG(transfer, trans_p, transferprog, NULL, NULL); + CBREG(transfer, trans_destroy, transferdestroyed, NULL, NULL); + return(0); +} + +static struct uiuser *newuser(wchar_t *name, unsigned long perms) +{ + struct uiuser *new; + + new = smalloc(sizeof(*new)); + new->used = 0; + new->name = swcsdup(name); + new->perms = perms; + new->delete = 0; + new->next = users; + new->prev = NULL; + if(users != NULL) + users->prev = new; + users = new; + return(new); +} + +static void freeuser(struct uiuser *user) +{ + if(user->next != NULL) + user->next->prev = user->prev; + if(user->prev != NULL) + user->prev->next = user->next; + if(user == users) + users = user->next; + free(user->name); + free(user); +} + +static int conf_user(int argc, wchar_t **argv) +{ + int i, perms, permmod; + struct uiuser *user; + wchar_t *p; + + if(argc < 3) + { + flog(LOG_WARNING, "not enough arguments given for user command"); + return(1); + } + perms = 0; + for(i = 2; i < argc; i++) + { + if(!iswalpha(argv[i][0])) + p = argv[i] + 1; + else + p = argv[i]; + if(!wcscmp(p, L"disallow")) + permmod = PERM_DISALLOW; + if(!wcscmp(p, L"admin")) + permmod = PERM_ADMIN; + if(!wcscmp(p, L"fnetctl")) + permmod = PERM_FNETCTL; + if(!wcscmp(p, L"trans")) + permmod = PERM_TRANS; + if(!wcscmp(p, L"transcu")) + permmod = PERM_TRANSCU; + if(!wcscmp(p, L"chat")) + permmod = PERM_CHAT; + if(!wcscmp(p, L"srch")) + permmod = PERM_SRCH; + if(!wcscmp(p, L"all")) + permmod = ~0; + if(argv[i][0] == L'-') + perms &= ~permmod; + else + perms |= permmod; + } + if((user = finduser(argv[1])) == NULL) + { + newuser(argv[1], perms); + } else { + user->delete = 0; + user->perms = perms; + } + return(0); +} + +static void preinit(int hup) +{ + struct uiuser *user; + + if(!hup) + { + newuser(L"default", 0); + } else { + for(user = users; user != NULL; user = user->next) + { + if(!wcscmp(user->name, L"default")) + user->delete = 1; + } + } +} + +#ifdef HAVE_IPV6 +static struct sockaddr *getnameforport(int port, socklen_t *len) +{ + static struct sockaddr_in6 addr; + + memset(&addr, 0, sizeof(addr)); + addr.sin6_family = AF_INET6; + addr.sin6_port = htons(port); + addr.sin6_addr = in6addr_any; + if(len != NULL) + *len = sizeof(addr); + return((struct sockaddr *)&addr); +} +#else +static struct sockaddr *getnameforport(int port, socklen_t *len) +{ + static struct sockaddr_in addr; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + if(len != NULL) + *len = sizeof(addr); + return((struct sockaddr *)&addr); +} +#endif + +static int portupdate(struct configvar *var, void *uudata) +{ + struct sockaddr *addr; + socklen_t addrlen; + struct socket *newsock; + + addr = getnameforport(var->val.num, &addrlen); + if((uisocket = netcslistenlocal(SOCK_STREAM, addr, addrlen, uiaccept, NULL)) == NULL) + { + flog(LOG_WARNING, "could not create new UI socket, reverting to old: %s", strerror(errno)); + return(0); + } + if(uisocket != NULL) + putsock(uisocket); + uisocket = newsock; + return(0); +} + +static int init(int hup) +{ + struct sockaddr *addr; + socklen_t addrlen; + struct uiuser *user, *next; + + if(hup) + { + for(user = users; user != NULL; user = next) + { + next = user->next; + if(user->delete) + freeuser(user); + } + } + if(!hup) + { + if(uisocket != NULL) + putsock(uisocket); + addr = getnameforport(confgetint("ui", "port"), &addrlen); + if((uisocket = netcslistenlocal(SOCK_STREAM, addr, addrlen, uiaccept, NULL)) == NULL) + { + flog(LOG_CRIT, "could not create UI socket: %s", strerror(errno)); + return(1); + } + CBREG(confgetvar("ui", "port"), conf_update, portupdate, NULL, NULL); + GCBREG(newfncb, newfnetnode, NULL); + GCBREG(newtransfercb, newtransfernotify, NULL); + } + return(0); +} + +static int run(void) +{ + int i, id; + struct uidata *data, *next; + struct qcommand *qcmd; + struct notif *notif, *nnotif; + wchar_t buf[64]; + + for(data = actives; data != NULL; data = next) + { + next = data->next; + if(data->close) + freeuidata(data); + } + for(data = actives; data != NULL; data = data->next) + { + for(notif = data->fnotif; notif != NULL; notif = nnotif) + { + nnotif = notif->next; + if(notif->state == NOTIF_WAIT) + continue; + id = -1; + for(i = 0; i < notif->argc; i++) + { + if(notif->argv[i].dt == NOTIF_ID) + { + id = notif->argv[i].d.n; + break; + } + } + if(findnotif(notif->prev, 0, -1, notif->code, id) != NULL) + continue; + sq(data->sk, 2, L"%%i", notif->code, NULL); + for(i = 0; i < notif->argc; i++) + { + switch(notif->argv[i].dt) + { + case NOTIF_INT: + case NOTIF_ID: + sq(data->sk, 2, L"%%i", notif->argv[i].d.n, NULL); + break; + case NOTIF_STR: + if(notif->argv[i].d.s[0] == L'%') + sq(data->sk, 2, L"%%s", notif->argv[i].d.s, NULL); + else + sq(data->sk, 2, notif->argv[i].d.s, NULL); + break; + case NOTIF_FLOAT: + swprintf(buf, 64, L"%f", notif->argv[i].d.d); + sq(data->sk, 2, buf, NULL); + break; + } + } + sq(data->sk, 0, NULL); + if(notif->rlimit != 0) + { + notif->state = NOTIF_WAIT; + notif->exptimer = timercallback(ntime() + notif->rlimit, (void (*)(int, void *))notifexpire, notif); + } else { + freenotif(notif); + } + } + if((qcmd = unlinkqcmd(data)) != NULL) + { + qcmd->cmd->handler(data->sk, data, qcmd->argc, qcmd->argv); + freequeuecmd(qcmd); + return(1); + } + } + return(0); +} + +static void terminate(void) +{ + while(users != NULL) + freeuser(users); +} + +static struct configvar myvars[] = +{ + {CONF_VAR_BOOL, "onlylocal", {.num = 1}}, + {CONF_VAR_INT, "port", {.num = 1500}}, + {CONF_VAR_INT, "uitos", {.num = SOCK_TOS_MINDELAY}}, + {CONF_VAR_STRING, "filtercmd", {.str = L"dc-filtercmd"}}, + {CONF_VAR_END} +}; + +static struct configcmd mycmds[] = +{ + {"user", conf_user}, + {NULL} +}; + +static struct module me = +{ + .name = "ui", + .conf = + { + .vars = myvars, + .cmds = mycmds + }, + .preinit = preinit, + .init = init, + .run = run, + .terminate = terminate +}; + +MODULE(me) diff --git a/daemon/uiretref b/daemon/uiretref new file mode 100644 index 0000000..8d10b2c --- /dev/null +++ b/daemon/uiretref @@ -0,0 +1,32 @@ +500 - Command not found +501 - Wrong number of arguments +502 - Unauthorized request +503 - Already logged in +504 - Could not convert to character set +505 - Internal error +506 - Authentication error +507 - Command not expected +508 - No such authentication mechanism +509 - Malformed argument +510 - No such fnetnode +511 - No such fnet +512 - No such transfer +513 - Not supported for this fnet +514 - No such search + +600 - Chat receipt +601 - FN state change +602 - FN name change +603 - FN destroy +604 - FN create +605 - FN num peers change +610 - Transfer create +611 - Transfer state change +612 - Transfer nick change +613 - Transfer size change +614 - Transfer error update +615 - Transfer progress +616 - Transfer path change +620 - Search ETA change +621 - Search commit +622 - Search result diff --git a/daemon/utils.c b/daemon/utils.c new file mode 100644 index 0000000..216ae77 --- /dev/null +++ b/daemon/utils.c @@ -0,0 +1,737 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include "utils.h" +#include "log.h" + +static char *base64set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +static int base64rev[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; +static char *base32set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; +static int base32rev[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, +}; + +char *vsprintf2(char *format, va_list al) +{ + int ret; + char *buf; + + ret = vsnprintf(NULL, 0, format, al); + if((buf = malloc(ret + 1)) == NULL) + { + LOGOOM(ret + 1); + return(NULL); + } + vsnprintf(buf, ret + 1, format, al); + return(buf); +} + +char *sprintf2(char *format, ...) +{ + va_list args; + char *buf; + + va_start(args, format); + buf = vsprintf2(format, args); + va_end(args); + return(buf); +} + +wchar_t *vswprintf2(wchar_t *format, va_list al) +{ + int ret; + wchar_t *buf; + size_t bufsize; + + buf = smalloc(sizeof(wchar_t) * (bufsize = 1024)); + while((ret = vswprintf(buf, bufsize, format, al)) < 0) + buf = srealloc(buf, sizeof(wchar_t) * (bufsize *= 2)); + if(bufsize > ret + 1) + buf = srealloc(buf, sizeof(wchar_t) * (ret + 1)); + return(buf); +} + +wchar_t *swprintf2(wchar_t *format, ...) +{ + va_list args; + wchar_t *buf; + + va_start(args, format); + buf = vswprintf2(format, args); + va_end(args); + return(buf); +} + +wchar_t *icmbstowcs(char *mbs, char *charset) +{ + int ret; + char *buf; + char *p, *p2; + size_t len1, len2, bufsize, data; + iconv_t cd; + + len1 = strlen(mbs) + 1; + bufsize = len2 = len1 * sizeof(wchar_t); + if((buf = malloc(bufsize)) == NULL) + { + LOGOOM(bufsize); + return(NULL); + } + if(charset == NULL) + charset = nl_langinfo(CODESET); + if((cd = iconv_open("wchar_t", charset)) == (iconv_t)-1) + { + flog(LOG_ERR, "icmbstowcs: could not open iconv structure for %s: %s", charset, strerror(errno)); + free(buf); + return(NULL); + } + p = buf; + while(len1 > 0) + { + ret = iconv(cd, &mbs, &len1, &p, &len2); + if(ret < 0) + { + if(errno == E2BIG) + { + data = p - buf; + len2 += 128; + bufsize += 128; + if((p2 = realloc(buf, bufsize)) == NULL) + { + LOGOOM(bufsize); + free(buf); + iconv_close(cd); + return(NULL); + } + buf = p2; + p = buf + data; + } else { + free(buf); + iconv_close(cd); + return(NULL); + } + } + } + if(len2 > 0) + buf = realloc(buf, p - buf); + iconv_close(cd); + return((wchar_t *)buf); +} + +wchar_t *icsmbstowcs(char *mbs, char *charset, wchar_t *def) +{ + static wchar_t *buf = NULL; + + if(buf != NULL) + free(buf); + if((buf = icmbstowcs(mbs, charset)) == NULL) + { + if(*def == '~') + { + flog(LOG_WARNING, "icsmbstowcs: could not convert wcs string into charset %s: %s", charset, strerror(errno)); + def++; + } + return(def); + } + return(buf); +} + +char *icwcstombs(wchar_t *wcs, char *charset) +{ + int ret; + char *buf; + char *p, *p2; + size_t len1, len2, bufsize, data; + iconv_t cd; + + len1 = sizeof(wchar_t) * (wcslen(wcs) + 1); + bufsize = len2 = len1; + if((buf = malloc(bufsize)) == NULL) + { + LOGOOM(bufsize); + return(NULL); + } + if(charset == NULL) + charset = nl_langinfo(CODESET); + if((cd = iconv_open(charset, "wchar_t")) == (iconv_t)-1) + { + flog(LOG_ERR, "icwcstombs: could not open iconv structure for %s: %s", charset, strerror(errno)); + free(buf); + return(NULL); + } + p = buf; + while(len1 > 0) + { + ret = iconv(cd, (char **)&wcs, &len1, &p, &len2); + if(ret < 0) + { + if(errno == E2BIG) + { + data = p - buf; + len2 += 128; + bufsize += 128; + if((p2 = realloc(buf, bufsize)) == NULL) + { + LOGOOM(bufsize); + free(buf); + iconv_close(cd); + return(NULL); + } + buf = p2; + p = buf + data; + } else { + free(buf); + iconv_close(cd); + return(NULL); + } + } + } + if(len2 > 0) + buf = realloc(buf, p - buf); + iconv_close(cd); + return(buf); +} + +char *icswcstombs(wchar_t *wcs, char *charset, char *def) +{ + static char *buf = NULL; + + if(buf != NULL) + free(buf); + if((buf = icwcstombs(wcs, charset)) == NULL) + { + if(*def == '~') + { + flog(LOG_WARNING, "icswcstombs: could not convert mbs string from charset %s: %s", charset, strerror(errno)); + def++; + } + return(def); + } + return(buf); +} + +wchar_t *wcstolower(wchar_t *wcs) +{ + wchar_t *p; + + for(p = wcs; *p != L'\0'; p++) + *p = towlower(*p); + return(wcs); +} + +wchar_t ucptowc(int ucp) +{ + int ret; + unsigned long ucpbuf; + char *buf; + char *mbsp, *p, *p2; + wchar_t res; + size_t len1, len2, bufsize, data; + iconv_t cd; + + ucpbuf = htonl(ucp); + mbsp = (char *)&ucpbuf; + len1 = 4; + bufsize = len2 = len1 * sizeof(wchar_t); + if((buf = malloc(bufsize)) == NULL) + { + LOGOOM(bufsize); + return(L'\0'); + } + if((cd = iconv_open("wchar_t", "UCS-4BE")) == (iconv_t)-1) + { + flog(LOG_ERR, "ucptowc: could not open iconv structure for UCS-4BE: %s", strerror(errno)); + free(buf); + return(L'\0'); + } + p = buf; + while(len1 > 0) + { + ret = iconv(cd, &mbsp, &len1, &p, &len2); + if(ret < 0) + { + if(errno == E2BIG) + { + data = p - buf; + len2 += 128; + bufsize += 128; + if((p2 = realloc(buf, bufsize)) == NULL) + { + LOGOOM(bufsize); + free(buf); + iconv_close(cd); + return(L'\0'); + } + buf = p2; + p = buf + data; + } else { + free(buf); + iconv_close(cd); + return(L'\0'); + } + } + } + if(len2 > 0) + buf = realloc(buf, p - buf); + iconv_close(cd); + res = *(wchar_t *)buf; + free(buf); + return(res); +} + +void _sizebuf(void **buf, size_t *bufsize, size_t reqsize, size_t elsize, int algo) +{ + if(*bufsize >= reqsize) + return; + switch(algo) + { + case 0: + *buf = srealloc(*buf, elsize * ((*bufsize) = reqsize)); + break; + case 1: + if(*bufsize == 0) + *bufsize = 1; + while(*bufsize < reqsize) + *bufsize <<= 1; + *buf = srealloc(*buf, elsize * (*bufsize)); + break; + } +} + +double ntime(void) +{ + struct timeval tv; + + gettimeofday(&tv, NULL); + return((double)tv.tv_sec + ((double)tv.tv_usec / 1000000.0)); +} + +int wcsexists(wchar_t *h, wchar_t *n) +{ + int i, o, nl, hl; + wchar_t *ln, *lh; + + ln = alloca(sizeof(*ln) * (nl = wcslen(n))); + for(i = 0; i < nl; i++) + ln[i] = towlower(n[i]); + lh = alloca(sizeof(*lh) * (hl = wcslen(h))); + if(nl > hl) + return(0); + for(i = 0; i < nl; i++) + lh[i] = towlower(h[i]); + i = 0; + while(1) + { + for(o = 0; o < nl; o++) + { + if(lh[i + o] != ln[o]) + break; + } + if(o == nl) + return(1); + if(i == hl - nl) + return(0); + lh[i + nl] = towlower(h[i + nl]); + i++; + } +} + +#ifndef HAVE_WCSCASECMP +int wcscasecmp(const wchar_t *s1, const wchar_t *s2) +{ + while(towlower(*s1) == towlower(*s2)) + { + if(*s1 == L'\0') + return(0); + } + return(towlower(*s1) - towlower(*s2)); +} +#endif + +char *hexencode(char *data, size_t datalen) +{ + char *buf, this; + size_t bufsize, bufdata; + int dig; + + buf = NULL; + bufsize = bufdata = 0; + for(; datalen > 0; datalen--, data++) + { + dig = (*data & 0xF0) >> 4; + if(dig > 9) + this = 'A' + dig - 10; + else + this = dig + '0'; + addtobuf(buf, this); + dig = *data & 0x0F; + if(dig > 9) + this = 'A' + dig - 10; + else + this = dig + '0'; + addtobuf(buf, this); + } + addtobuf(buf, 0); + return(buf); +} + +char *hexdecode(char *data, size_t *len) +{ + char *buf, this; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + for(; *data; data++) + { + if((*data >= 'A') && (*data <= 'F')) + { + this = (this & 0x0F) | ((*data - 'A' + 10) << 4); + } else if((*data >= '0') && (*data <= '9')) { + this = (this & 0x0F) | ((*data - '0') << 4); + } else { + if(buf != NULL) + free(buf); + return(NULL); + } + data++; + if(!*data) + { + if(buf != NULL) + free(buf); + return(NULL); + } + if((*data >= 'A') && (*data <= 'F')) + { + this = (this & 0xF0) | (*data - 'A' + 10); + } else if((*data >= '0') && (*data <= '9')) { + this = (this & 0xF0) | (*data - '0'); + } else { + if(buf != NULL) + free(buf); + return(NULL); + } + addtobuf(buf, this); + } + addtobuf(buf, 0); + if(len != NULL) + *len = bufdata - 1; + return(buf); +} + +char *base64encode(char *data, size_t datalen) +{ + char *buf; + size_t bufsize, bufdata; + + if(datalen == 0) + return(sstrdup("")); + buf = NULL; + bufsize = bufdata = 0; + while(datalen >= 3) + { + addtobuf(buf, base64set[(data[0] & 0xfc) >> 2]); + addtobuf(buf, base64set[((data[0] & 0x03) << 4) | ((data[1] & 0xf0) >> 4)]); + addtobuf(buf, base64set[((data[1] & 0x0f) << 2) | ((data[2] & 0xc0) >> 6)]); + addtobuf(buf, base64set[data[2] & 0x3f]); + datalen -= 3; + data += 3; + } + if(datalen == 1) + { + addtobuf(buf, base64set[(data[0] & 0xfc) >> 2]); + addtobuf(buf, base64set[(data[0] & 0x03) << 4]); + bufcat(buf, "==", 2); + } + if(datalen == 2) + { + addtobuf(buf, base64set[(data[0] & 0xfc) >> 2]); + addtobuf(buf, base64set[((data[0] & 0x03) << 4) | ((data[1] & 0xf0) >> 4)]); + addtobuf(buf, base64set[(data[1] & 0x0f) << 2]); + addtobuf(buf, '='); + } + addtobuf(buf, 0); + return(buf); +} + +char *base64decode(char *data, size_t *datalen) +{ + int b, c; + char *buf, cur; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + cur = 0; + b = 8; + for(; *data > 0; data++) + { + c = (int)(unsigned char)*data; + if(c == '=') + break; + if(base64rev[c] == -1) + { + if(buf != NULL) + free(buf); + return(NULL); + } + b -= 6; + if(b <= 0) + { + cur |= base64rev[c] >> -b; + addtobuf(buf, cur); + b += 8; + cur = 0; + } + cur |= base64rev[c] << b; + } + if(datalen != NULL) + *datalen = bufdata; + addtobuf(buf, 0); + return(buf); +} + +char *base32encode(char *data, size_t datalen) +{ + char *buf; + size_t bufsize, bufdata; + + if(datalen == 0) + return(sstrdup("")); + buf = NULL; + bufsize = bufdata = 0; + while(datalen >= 5) + { + addtobuf(buf, base32set[((data[0] & 0xf8) >> 3)]); + addtobuf(buf, base32set[((data[0] & 0x07) << 2) | ((data[1] & 0xc0) >> 6)]); + addtobuf(buf, base32set[((data[1] & 0x3e) >> 1)]); + addtobuf(buf, base32set[((data[1] & 0x01) << 4) | ((data[2] & 0xf0) >> 4)]); + addtobuf(buf, base32set[((data[2] & 0x0f) << 1) | ((data[3] & 0x80) >> 7)]); + addtobuf(buf, base32set[((data[3] & 0x7c) >> 2)]); + addtobuf(buf, base32set[((data[3] & 0x03) << 3) | ((data[4] & 0xe0) >> 5)]); + addtobuf(buf, base32set[data[4] & 0x1f]); + datalen -= 5; + data += 5; + } + if(datalen == 1) + { + addtobuf(buf, base32set[((data[0] & 0xf8) >> 3)]); + addtobuf(buf, base32set[((data[0] & 0x07) << 2)]); + bufcat(buf, "======", 6); + } + if(datalen == 2) + { + addtobuf(buf, base32set[((data[0] & 0xf8) >> 3)]); + addtobuf(buf, base32set[((data[0] & 0x07) << 2) | ((data[1] & 0xc0) >> 6)]); + addtobuf(buf, base32set[((data[1] & 0x3e) >> 1)]); + addtobuf(buf, base32set[((data[1] & 0x01) << 4)]); + bufcat(buf, "====", 4); + } + if(datalen == 3) + { + addtobuf(buf, base32set[((data[0] & 0xf8) >> 3)]); + addtobuf(buf, base32set[((data[0] & 0x07) << 2) | ((data[1] & 0xc0) >> 6)]); + addtobuf(buf, base32set[((data[1] & 0x3e) >> 1)]); + addtobuf(buf, base32set[((data[1] & 0x01) << 4) | ((data[2] & 0xf0) >> 4)]); + addtobuf(buf, base32set[((data[2] & 0x0f) << 1)]); + bufcat(buf, "===", 3); + } + if(datalen == 4) + { + addtobuf(buf, base32set[((data[0] & 0xf8) >> 3)]); + addtobuf(buf, base32set[((data[0] & 0x07) << 2) | ((data[1] & 0xc0) >> 6)]); + addtobuf(buf, base32set[((data[1] & 0x3e) >> 1)]); + addtobuf(buf, base32set[((data[1] & 0x01) << 4) | ((data[2] & 0xf0) >> 4)]); + addtobuf(buf, base32set[((data[2] & 0x0f) << 1) | ((data[3] & 0x80) >> 7)]); + addtobuf(buf, base32set[((data[3] & 0x7c) >> 2)]); + addtobuf(buf, base32set[((data[3] & 0x03) << 3)]); + bufcat(buf, "=", 1); + } + addtobuf(buf, 0); + return(buf); +} + +char *base32decode(char *data, size_t *datalen) +{ + int b, c; + char *buf, cur; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + cur = 0; + b = 8; + for(; *data > 0; data++) + { + c = (int)(unsigned char)*data; + if(c == '=') + break; + if(base32rev[c] == -1) + { + if(buf != NULL) + free(buf); + return(NULL); + } + b -= 5; + if(b <= 0) + { + cur |= base32rev[c] >> -b; + addtobuf(buf, cur); + b += 8; + cur = 0; + } + cur |= base32rev[c] << b; + } + if(datalen != NULL) + *datalen = bufdata; + addtobuf(buf, 0); + return(buf); +} + +void _freeparr(void **arr) +{ + void **buf; + + if(arr == NULL) + return; + for(buf = arr; *buf != NULL; buf++) + free(*buf); + free(arr); +} + +int _parrlen(void **arr) +{ + int i; + + if(arr == NULL) + return(0); + for(i = 0; *arr != NULL; arr++) + i++; + return(i); +} + +char *getetcpath(char *binpath) +{ + char *etcpath, *p; + size_t etcpathsize, etcpathdata; + + etcpath = NULL; + etcpathsize = etcpathdata = 0; + do + { + for(p = binpath; *p && (*p != ':'); p++); + for(; (p >= binpath) && (*p != '/'); p--); + if(p >= binpath) + { + if(etcpathdata > 0) + addtobuf(etcpath, ':'); + bufcat(etcpath, binpath, p - binpath + 1); + bufcat(etcpath, "etc", 3); + } + } while((binpath = strchr(binpath, ':')) != NULL); + addtobuf(etcpath, 0); + return(etcpath); +} + +char *findfile(char *gname, char *uname, char *homedir) +{ + char *path, *binpath, *etcpath, *p; + + if((homedir != NULL) && ((path = sprintf2("%s/.%s", homedir, uname)) != NULL)) + { + if(!access(path, F_OK)) + return(path); + free(path); + } + if(gname != NULL) + { + if(strchr(gname, '/') != NULL) + { + if(!access(gname, F_OK)) + return(sstrdup(gname)); + } else { + if((binpath = getenv("PATH")) == NULL) + etcpath = sstrdup("/usr/local/etc:/etc:/usr/etc"); + else + etcpath = getetcpath(binpath); + for(p = strtok(etcpath, ":"); p != NULL; p = strtok(NULL, ":")) + { + if((path = sprintf2("%s/%s", p, gname)) != NULL) + { + if(!access(path, F_OK)) + { + free(etcpath); + return(path); + } + free(path); + } + } + free(etcpath); + } + } + return(NULL); +} diff --git a/daemon/utils.h b/daemon/utils.h new file mode 100644 index 0000000..b8b6803 --- /dev/null +++ b/daemon/utils.h @@ -0,0 +1,193 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ +#ifndef _UTILS_H +#define _UTILS_H + +#include +#include +#include +#include "log.h" + +/* "Safe" functions */ +#define smalloc(size) ({void *__result__; ((__result__ = malloc(size)) == NULL)?({LOGOOM(size); abort(); (void *)0;}):__result__;}) +#define srealloc(ptr, size) ({void *__result__; ((__result__ = realloc((ptr), (size))) == NULL)?({LOGOOM(size); abort(); (void *)0;}):__result__;}) +#define swcsdup(wcs) ((wchar_t *)wcscpy(smalloc(sizeof(wchar_t) * (wcslen(wcs) + 1)), (wcs))) +#define sstrdup(str) ((char *)strcpy(smalloc(strlen(str) + 1), (str))) + +#define CBCHAIN(name, args...) \ +struct cbchain_ ## name { \ + struct cbchain_ ## name *next, *prev; \ + int (*func)(args, void *data); \ + void (*destroy)(void *data); \ + void *data; \ +} * name + +#define GCBCHAIN(name, args...) \ +struct cbchain_ ## name * name = NULL + +#define EGCBCHAIN(name, args...) \ +extern struct cbchain_ ## name { \ + struct cbchain_ ## name *next, *prev; \ + int (*func)(args, void *data); \ + void *data; \ +} * name + +extern int vswprintf (wchar_t *__restrict __s, size_t __n, + __const wchar_t *__restrict __format, + __gnuc_va_list __arg); +extern int swprintf (wchar_t *__restrict __s, size_t __n, + __const wchar_t *__restrict __format, ...); + +char *vsprintf2(char *format, va_list al); +char *sprintf2(char *format, ...) +#if defined(__GNUC__) && 0 + __attribute__ ((format (printf, 1, 2))) +#endif + +; +wchar_t *vswprintf2(wchar_t *format, va_list al); +wchar_t *swprintf2(wchar_t *format, ...); +wchar_t *icmbstowcs(char *mbs, char *charset); +wchar_t *icsmbstowcs(char *mbs, char *charset, wchar_t *def); +char *icwcstombs(wchar_t *wcs, char *charset); +char *icswcstombs(wchar_t *wcs, char *charset, char *def); +wchar_t *wcstolower(wchar_t *wcs); +wchar_t ucptowc(int ucp); +void _sizebuf(void **buf, size_t *bufsize, size_t reqsize, size_t elsize, int algo); +double ntime(void); +int wcsexists(wchar_t *h, wchar_t *n); +#ifndef HAVE_WCSCASECMP +int wcscasecmp(const wchar_t *s1, const wchar_t *s2); +#endif +char *hexencode(char *data, size_t datalen); +char *hexdecode(char *data, size_t *len); +char *base64encode(char *data, size_t datalen); +char *base64decode(char *data, size_t *datalen); +char *base32encode(char *data, size_t datalen); +char *base32decode(char *data, size_t *datalen); +void _freeparr(void **arr); +int _parrlen(void **arr); +char *findfile(char *gname, char *uname, char *homedir); + +#define sizebuf(b, bs, rs, es, a) _sizebuf((void **)(b), (bs), (rs), (es), (a)) +#define sizebuf2(b, rs, a) _sizebuf((void **)(&(b)), &(b ## size), (rs), sizeof(*(b)), (a)) +#define addtobuf(b, c) \ +do { \ + _sizebuf((void **)(&(b)), &(b ## size), (b ## data) + 1, sizeof(*(b)), 1); \ + (b)[(b ## data)++] = (c); \ +} while(0) +#define bufcat(d, s, n) \ +do { \ + size_t __bufcat_size__; \ + __bufcat_size__ = (n); \ + _sizebuf((void **)(&(d)), &(d ## size), (d ## data) + __bufcat_size__, sizeof(*(d)), 1); \ + memcpy((d) + (d ## data), (s), sizeof(*(d)) * __bufcat_size__); \ + (d ## data) += __bufcat_size__; \ +} while (0) + +#define freeparr(parr) _freeparr((void **)(parr)) +#define parrlen(parr) _parrlen((void **)(parr)) + +#define CBREG(obj, name, funca, destroya, dataa) \ +do { \ + struct cbchain_ ## name *__new_cb__; \ + __new_cb__ = smalloc(sizeof(*__new_cb__)); \ + __new_cb__->func = funca; \ + __new_cb__->destroy = destroya; \ + __new_cb__->data = dataa; \ + __new_cb__->prev = NULL; \ + __new_cb__->next = (obj)->name; \ + (obj)->name = __new_cb__; \ +} while(0) + +#define CBUNREG(obj, name, dataa) \ +do { \ + struct cbchain_ ## name *__cur__; \ + for(__cur__ = (obj)->name; __cur__ != NULL; __cur__ = __cur__->next) { \ + if(__cur__->data == (dataa)) { \ + if(__cur__->destroy != NULL) \ + __cur__->destroy(__cur__->data); \ + if(__cur__->prev != NULL) \ + __cur__->prev->next = __cur__->next; \ + if(__cur__->next != NULL) \ + __cur__->next->prev = __cur__->prev; \ + if(__cur__ == (obj)->name) \ + (obj)->name = __cur__->next; \ + free(__cur__); \ + break; \ + } \ + } \ +} while(0) + +#define GCBREG(name, funca, dataa) \ +do { \ + struct cbchain_ ## name *__new_cb__; \ + __new_cb__ = smalloc(sizeof(*__new_cb__)); \ + __new_cb__->func = funca; \ + __new_cb__->data = dataa; \ + __new_cb__->prev = NULL; \ + __new_cb__->next = name; \ + name = __new_cb__; \ +} while(0) + +#define CBCHAININIT(obj, name) (obj)->name = NULL + +#define CBCHAINFREE(obj, name) \ +do { \ + struct cbchain_ ## name *__cur__; \ + while((__cur__ = (obj)->name) != NULL) { \ + (obj)->name = __cur__->next; \ + if(__cur__->destroy != NULL) \ + __cur__->destroy(__cur__->data); \ + free(__cur__); \ + } \ +} while(0) + +#define CBCHAINDOCB(obj, name, args...) \ +do { \ + struct cbchain_ ## name *__cur__, *__next__; \ + for(__cur__ = (obj)->name; __cur__ != NULL; __cur__ = __next__) { \ + __next__ = __cur__->next; \ + if(__cur__->func(args, __cur__->data)) { \ + if(__cur__->next != NULL) \ + __cur__->next->prev = __cur__->prev; \ + if(__cur__->prev != NULL) \ + __cur__->prev->next = __cur__->next; \ + if(__cur__ == (obj)->name) \ + (obj)->name = __cur__->next; \ + free(__cur__); \ + } \ + } \ +} while(0) + +#define GCBCHAINDOCB(name, args...) \ +({ \ + struct cbchain_ ## name *__cur__; \ + int __ret__; \ + __ret__ = 0; \ + for(__cur__ = name; __cur__ != NULL; __cur__ = __cur__->next) { \ + if(__cur__->func(args, __cur__->data)) { \ + __ret__ = 1; \ + break; \ + } \ + } \ + __ret__; \ +}) + +#endif diff --git a/include/CVS/Entries b/include/CVS/Entries new file mode 100644 index 0000000..25536c8 --- /dev/null +++ b/include/CVS/Entries @@ -0,0 +1,2 @@ +/Makefile.am/1.3/Wed Aug 25 13:13:58 2004// +D/doldaconnect//// diff --git a/include/CVS/Repository b/include/CVS/Repository new file mode 100644 index 0000000..9845500 --- /dev/null +++ b/include/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/include diff --git a/include/CVS/Root b/include/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/include/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/include/Makefile.am b/include/Makefile.am new file mode 100644 index 0000000..4c6fb9a --- /dev/null +++ b/include/Makefile.am @@ -0,0 +1,2 @@ +pkginclude_HEADERS=doldaconnect/uilib.h doldaconnect/uimisc.h doldaconnect/utils.h +EXTRA_DIST=doldaconnect diff --git a/include/doldaconnect/CVS/Entries b/include/doldaconnect/CVS/Entries new file mode 100644 index 0000000..a6ad5de --- /dev/null +++ b/include/doldaconnect/CVS/Entries @@ -0,0 +1,4 @@ +/uilib.h/1.4/Fri Aug 6 19:11:23 2004// +/uimisc.h/1.5/Fri Aug 13 11:42:30 2004// +/utils.h/1.2/Wed Jul 28 00:00:38 2004// +D diff --git a/include/doldaconnect/CVS/Repository b/include/doldaconnect/CVS/Repository new file mode 100644 index 0000000..f2b63cf --- /dev/null +++ b/include/doldaconnect/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/include/doldaconnect diff --git a/include/doldaconnect/CVS/Root b/include/doldaconnect/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/include/doldaconnect/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/include/doldaconnect/uilib.h b/include/doldaconnect/uilib.h new file mode 100644 index 0000000..d67afb3 --- /dev/null +++ b/include/doldaconnect/uilib.h @@ -0,0 +1,56 @@ +#ifndef _UILIB_H +#define _UILIB_H + +#include + +struct dc_response +{ + struct dc_response *next, *prev; + int code, tag; + wchar_t *cmdname; + void *data; + void *internal; + struct + { + int argc; + wchar_t **argv; + } *rlines; + int linessize; + int numlines; + int curline; +}; + +struct dc_intresp +{ + int code; + int argc; + struct + { + int type; + union + { + int num; + wchar_t *str; + double flnum; + } val; + } *argv; +}; + +int dc_init(void); +void dc_cleanup(void); +void dc_disconnect(void); +void dc_freeresp(struct dc_response *resp); +struct dc_response *dc_getresp(void); +struct dc_response *dc_gettaggedresp(int tag); +struct dc_response *dc_gettaggedrespsync(int tag); +int dc_wantwrite(void); +int dc_getstate(void); +int dc_queuecmd(int (*callback)(struct dc_response *), void *data, ...); +int dc_handleread(void); +int dc_handlewrite(void); +int dc_connect(char *host, int port); +struct dc_intresp *dc_interpret(struct dc_response *resp); +void dc_freeires(struct dc_intresp *ires); +const char *dc_gethostname(void); + +#endif diff --git a/include/doldaconnect/uimisc.h b/include/doldaconnect/uimisc.h new file mode 100644 index 0000000..a7bc3e7 --- /dev/null +++ b/include/doldaconnect/uimisc.h @@ -0,0 +1,76 @@ +#ifndef _UIMISC_H +#define _UIMISC_H + +#define DC_LOGIN_ERR_SUCCESS 0 +#define DC_LOGIN_ERR_NOLOGIN 1 +#define DC_LOGIN_ERR_SERVER 2 +#define DC_LOGIN_ERR_USER 3 +#define DC_LOGIN_ERR_CONV 4 +#define DC_LOGIN_ERR_AUTHFAIL 5 + +#define DC_LOGIN_CONV_NOECHO 0 +#define DC_LOGIN_CONV_ECHO 1 +#define DC_LOGIN_CONV_INFO 2 +#define DC_LOGIN_CONV_ERROR 3 + +#define DC_FNN_STATE_SYN 0 +#define DC_FNN_STATE_HS 1 +#define DC_FNN_STATE_EST 2 +#define DC_FNN_STATE_DEAD 3 + +#define DC_TRNS_WAITING 0 +#define DC_TRNS_HS 1 +#define DC_TRNS_MAIN 2 +#define DC_TRNS_DONE 3 + +#define DC_TRNSD_UNKNOWN 0 +#define DC_TRNSD_UP 1 +#define DC_TRNSD_DOWN 2 + +#define DC_TRNSE_NOERROR 0 +#define DC_TRNSE_NOTFOUND 1 +#define DC_TRNSE_NOSLOTS 2 + +#include + +struct dc_fnetnode +{ + struct dc_fnetnode *next, *prev; + int id; + wchar_t *name; + wchar_t *fnet; + int state; + int numusers; + int found; + void (*destroycb)(struct dc_fnetnode *fn); + void *udata; +}; + +struct dc_transfer +{ + struct dc_transfer *next, *prev; + int id; + int dir, state; + wchar_t *peerid, *peernick; + wchar_t *path; + int size, curpos; + int found; + int error; + time_t errortime; + void (*destroycb)(struct dc_transfer *transfer); + void *udata; +}; + +void dc_loginasync(char *username, int useauthless, int (*conv)(int, wchar_t *, char **, void *), void (*callback)(int, wchar_t *, void *), void *udata); +struct dc_fnetnode *dc_findfnetnode(int id); +void dc_getfnlistasync(void (*callback)(int, void *), void *udata); +void dc_uimisc_handlenotify(struct dc_response *resp); +struct dc_transfer *dc_findtransfer(int id); +void dc_gettrlistasync(void (*callback)(int, void *), void *udata); +wchar_t **dc_lexsexpr(wchar_t *sexpr); +void dc_freewcsarr(wchar_t **arr); + +extern struct dc_fnetnode *dc_fnetnodes; +extern struct dc_transfer *dc_transfers; + +#endif diff --git a/include/doldaconnect/utils.h b/include/doldaconnect/utils.h new file mode 100644 index 0000000..60e06ad --- /dev/null +++ b/include/doldaconnect/utils.h @@ -0,0 +1,40 @@ +#ifndef _UTILS_H +#define _UTILS_H + +#include +#include + +/* "Safe" functions */ +#define smalloc(size) ({void *__result__; ((__result__ = malloc(size)) == NULL)?({exit(-1); (void *)0;}):__result__;}) +#define srealloc(ptr, size) ({void *__result__; ((__result__ = realloc((ptr), (size))) == NULL)?({exit(-1); (void *)0;}):__result__;}) +#define swcsdup(wcs) ((wchar_t *)wcscpy(smalloc(sizeof(wchar_t) * (wcslen(wcs) + 1)), (wcs))) +#define sstrdup(str) ((char *)strcpy(smalloc(strlen(str) + 1), (str))) + +char *vsprintf2(char *format, va_list al); +char *sprintf2(char *format, ...); +wchar_t *vswprintf2(wchar_t *format, va_list al); +wchar_t *swprintf2(wchar_t *format, ...); +wchar_t *icmbstowcs(char *mbs, char *charset); +wchar_t *icsmbstowcs(char *mbs, char *charset, wchar_t *def); +char *icwcstombs(wchar_t *wcs, char *charset); +char *icswcstombs(wchar_t *wcs, char *charset, char *def); +wchar_t *wcstolower(wchar_t *wcs); +void _sizebuf(void **buf, size_t *bufsize, size_t reqsize, size_t elsize, int algo); + +#define sizebuf(b, bs, rs, es, a) _sizebuf((void **)(b), (bs), (rs), (es), (a)) +#define sizebuf2(b, rs, a) _sizebuf((void **)(&(b)), &(b ## size), (rs), sizeof(*(b)), (a)) +#define addtobuf(b, c) \ +do { \ + _sizebuf((void **)(&(b)), &(b ## size), (b ## data) + 1, sizeof(*(b)), 1); \ + (b)[(b ## data)++] = (c); \ +} while(0) +#define bufcat(d, s, n) \ +do { \ + size_t __bufcat_size__; \ + __bufcat_size__ = (n); \ + _sizebuf((void **)(&(d)), &(d ## size), (d ## data) + __bufcat_size__, sizeof(*(d)), 1); \ + memcpy((d) + (d ## data), (s), sizeof(*(d)) * __bufcat_size__); \ + (d ## data) += __bufcat_size__; \ +} while (0) + +#endif diff --git a/lib/CVS/Entries b/lib/CVS/Entries new file mode 100644 index 0000000..01eba9f --- /dev/null +++ b/lib/CVS/Entries @@ -0,0 +1,9 @@ +/Makefile.am/1.5/Mon Oct 4 02:05:35 2004// +/initcmds.h/1.8/Mon Jan 24 01:31:54 2005// +/makecmds/1.2/Wed Aug 4 12:46:13 2004// +/uicmds/1.9/Mon Jan 24 01:32:02 2005// +/uilib.c/1.13/Mon May 9 23:16:56 2005// +/uilib.h/1.1.1.1/Tue May 11 15:48:00 2004// +/uimisc.c/1.7/Mon May 9 23:20:49 2005// +/utils.c/1.2/Wed Jul 28 00:00:47 2004// +D/guile//// diff --git a/lib/CVS/Repository b/lib/CVS/Repository new file mode 100644 index 0000000..9ae7dde --- /dev/null +++ b/lib/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/lib diff --git a/lib/CVS/Root b/lib/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/lib/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..2449ba7 --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,16 @@ +EXTRA_DIST = makecmds uicmds + +SUBDIRS=. @extlibs@ +DIST_SUBDIRS=guile + +lib_LTLIBRARIES = libdcui.la + +libdcui_la_SOURCES = uilib.c uimisc.c utils.c +libdcui_la_LIBADD = @KRB5_LDADD@ + +BUILT_SOURCES = initcmds.h + +initcmds.h: uicmds makecmds + ./makecmds initcmds.h + +AM_CPPFLAGS=-I$(top_srcdir)/include diff --git a/lib/guile/CVS/Entries b/lib/guile/CVS/Entries new file mode 100644 index 0000000..6c1ad48 --- /dev/null +++ b/lib/guile/CVS/Entries @@ -0,0 +1,5 @@ +/Makefile.am/1.2/Sat May 14 14:23:25 2005// +/autodl/1.16/Sun Oct 9 02:53:45 2005// +/chatlog/1.2/Wed Jul 13 00:24:24 2005// +/dolcon-guile.c/1.3/Fri Sep 24 00:48:41 2004// +D/dolcon//// diff --git a/lib/guile/CVS/Repository b/lib/guile/CVS/Repository new file mode 100644 index 0000000..328ead7 --- /dev/null +++ b/lib/guile/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/lib/guile diff --git a/lib/guile/CVS/Root b/lib/guile/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/lib/guile/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/lib/guile/Makefile.am b/lib/guile/Makefile.am new file mode 100644 index 0000000..caf7c84 --- /dev/null +++ b/lib/guile/Makefile.am @@ -0,0 +1,9 @@ +SUBDIRS=dolcon + +EXTRA_DIST=autodl chatlog + +lib_LTLIBRARIES=libdolcon-guile.la + +libdolcon_guile_la_SOURCES=dolcon-guile.c +libdolcon_guile_la_LIBADD=@GUILE_LDFLAGS@ $(top_srcdir)/lib/libdcui.la +libdolcon_guile_la_CPPFLAGS=@GUILE_CFLAGS@ diff --git a/lib/guile/autodl b/lib/guile/autodl new file mode 100755 index 0000000..f57423f --- /dev/null +++ b/lib/guile/autodl @@ -0,0 +1,547 @@ +#!/usr/bin/guile -s +!# + +(use-modules (dolcon ui)) +(use-modules (ice-9 pretty-print)) + +(define sr '()) +(define lastsearch 0) +(define info-searcheta 0) +(define info-numavail 0) +(define info-numreal 0) +(define info-numtotal 0) +(define lastparse 0) +(define srchid -1) +(define session '()) +(define trans '()) +(define dpeers '()) +(define lastdl 0) + +(define (logf msg) + (write-line msg (current-output-port)) + (catch 'system-error (lambda () + (fsync (current-output-port))) + (lambda (key . err) #f)) + ) + +(define (make-getopt opts optdesc) + (let ((arg opts) (curpos 0) (rest '())) + (lambda () + (if (eq? arg '()) rest + (let ((ret #f)) + (while (not ret) + (if (= curpos 0) + (if (eq? (string-ref (car arg) 0) #\-) + (set! curpos 1) + (begin + (set! rest (append rest (list (car arg)))) + (set! arg (cdr arg)) + (if (eq? arg '()) + (set! ret #t))))) + (if (> curpos 0) + (if (< curpos (string-length (car arg))) + (begin (set! ret (string-ref (car arg) curpos)) (set! curpos (+ curpos 1))) + (begin (set! curpos 0) (set! arg (cdr arg)) (if (eq? arg '()) (set! ret #t)))))) + (if (eq? ret #t) rest + (let ((opt (string-index optdesc ret))) + (if (eq? opt #f) (throw 'illegal-option ret) + (if (and (< opt (- (string-length optdesc) 1)) (eq? (string-ref optdesc (+ opt 1)) #\:)) + (let ((ret + (cons ret (let ((optarg + (if (< curpos (string-length (car arg))) + (substring (car arg) curpos) + (begin (set! arg (cdr arg)) (if (eq? arg '()) (throw 'requires-argument ret)) (car arg))))) + (set! arg (cdr arg)) optarg)))) + (set! curpos 0) + ret) + (list ret)))))))))) + +(define (ftime) + (let ((ctime (gettimeofday))) + (+ (car ctime) (/ (cdr ctime) 1000000)))) + +(define (wanttosearch) + (> (- (current-time) lastsearch) + (if (> (length trans) 0) 300 60)) + ) + +(define defspeed '()) +(let ((matchlist (list + (cons (make-regexp "^[][{}() ]*BBB" regexp/icase) 100000)))) + (set! defspeed + (lambda (sr) + (catch 'ret + (lambda () + (for-each (lambda (o) + (if (regexp-exec (car o) (cadr (cdr (assoc 'peer sr)))) + (throw 'ret (cdr o)))) + matchlist) + 15000) + (lambda (sig ret) + ret)) + ))) + +(define (sr-less? sr1 sr2) + (let ((s1 (if (cdr (assoc 'speed sr1)) (cdr (assoc 'speed sr1)) (defspeed sr1))) + (s2 (if (cdr (assoc 'speed sr2)) (cdr (assoc 'speed sr2)) (defspeed sr2)))) + (if (= s1 s2) + (< (cdr (assoc 'resptime sr1)) (cdr (assoc 'resptime sr2))) + (> s1 s2))) + ) + +(define (srg-less? g1 g2) + (or (> (length (cdr g1)) (length (cdr g2))) + (and (= (length (cdr g1)) (length (cdr g2))) + (> (car g1) (car g2)))) + ) + +(define (gettrbysize size) + (catch 'ret + (lambda () + (for-each (lambda (o) + (if (= (cdr (assoc 'size (cdr o))) size) + (throw 'ret (cdr o)))) + trans) + #f) + (lambda (sig ret) + ret)) + ) + +(define (download sr) + (let ((resp #f)) + (let ((args (list "download" + (car (cdr (assoc 'peer sr))) + (cadr (cdr (assoc 'peer sr))) + (cdr (assoc 'filename sr)) + (cdr (assoc 'size sr))))) + (let ((tag (assoc 'tag session))) + (if tag (set! args (append args (list "tag" (cdr tag)))))) + (let ((uarg (assoc 'uarg session))) + (if uarg (set! args (append args (list "user" (cdr uarg)))))) + (set! resp (apply dc-ecmd-assert 200 args))) + (let ((id (car (dc-intresp resp)))) + (set! trans + (cons (cons id (list (assoc 'size sr) + (assoc 'peer sr) + (assoc 'filename sr) + (assoc 'resptime sr) + '(curpos . 0) + '(state . wait) + '(curspeed . #f) + '(lastpos . 0) + (cons 'id id) + (cons 'lasttime (current-time)) + (cons 'lastprog (current-time)))) + trans)) + (logf (string-append "downloading " + (cdr (assoc 'filename sr)) + " from " + (cadr (cdr (assoc 'peer sr))) + ", " + (number->string (cdr (assoc 'size sr))) + " bytes (id " + (number->string id) + ", " + (number->string (cdr (assoc 'slots sr))) + " slots), timing out in " + (number->string (max 10 (* (cdr (assoc 'resptime sr)) 2))) + " seconds")))) + (set! lastdl (current-time)) + ) + +(define (disablepeer peer) + (let ((newglist '()) (numrem 0)) + (for-each (lambda (g) + (let ((newlist '())) + (for-each (lambda (o) + (if (not (equal? (cdr (assoc 'peer o)) peer)) + (set! newlist (cons o newlist)) + (set! numrem (+ numrem 1)))) + (cdr g)) + (if (not (eq? newlist '())) + (set! newglist (cons (cons (car g) (sort newlist sr-less?)) newglist))))) + sr) + (set! sr (sort newglist srg-less?)) + (logf (string-append "disabled " (cadr peer) " and removed " (number->string numrem) " search results"))) + (let* ((dpa (assoc peer dpeers)) (dp (and (pair? dpa) (cdr dpa)))) + (if dp + (set-cdr! (assoc 'time dp) (current-time)) + (set! dpeers (cons (cons peer (list (cons 'time (current-time)) + (cons 'peer peer))) + dpeers)))) + ) + +(define (checktrans) + (let ((time (current-time))) + (for-each (lambda (o) + (if (and (memq (cdr (assoc 'state (cdr o))) '(wait hs)) + (> (- time (cdr (assoc 'lastprog (cdr o)))) (max 10 (* (cdr (assoc 'resptime (cdr o))) 2)))) + (begin (logf (string-append "transfer " (number->string (car o)) " timing out")) + (dc-ecmd-assert 200 "cancel" (car o)) + (disablepeer (cdr (assoc 'peer (cdr o)))) + (set! trans (assq-remove! trans (car o))))) + (if (and (eq? (cdr (assoc 'state (cdr o))) 'main) + (> (- time (cdr (assoc 'lastprog (cdr o)))) 60)) + (begin (logf (string-append "transfer " (number->string (car o)) " seems to have stalled")) + (dc-ecmd-assert 200 "cancel" (car o)) + (set! trans (assq-remove! trans (car o))))) + (if (and (eq? (cdr (assoc 'state (cdr o))) 'main) + (> (- (cdr (assoc 'lastprog (cdr o))) (cdr (assoc 'lasttime (cdr o)))) 20)) + (begin (set-cdr! (assoc 'curspeed (cdr o)) + (/ (- (cdr (assoc 'curpos (cdr o))) (cdr (assoc 'lastpos (cdr o)))) + (- (cdr (assoc 'lastprog (cdr o))) (cdr (assoc 'lasttime (cdr o)))))) + (set-cdr! (assoc 'lastpos (cdr o)) (cdr (assoc 'curpos (cdr o)))) + (set-cdr! (assoc 'lasttime (cdr o)) (cdr (assoc 'lastprog (cdr o))))))) + trans)) + ) + +(define (write-info-file) + (if (assoc 'info-file session) + (let ((op (open-output-file (cdr (assoc 'info-file session))))) + (write (list (cons 'numdl (length trans)) + (cons 'lastdl lastdl) + (cons 'availsr info-numavail) + (cons 'realsr info-numreal) + (cons 'totalsr info-numtotal) + (cons 'lastsrch lastsearch) + (cons 'srcheta info-searcheta)) + op) + (newline op) + (close-port op)))) + +(define (parseresults) + (logf (string-append "entering parseresults with " + (number->string + (apply + (map (lambda (o) (length (cdr o))) sr))) + " results in " + (number->string (length sr)) + " sizes")) + (let ((retval #t) (numreal 0) (numtotal 0) (numavail 0)) + (catch 'ret + (lambda () + (and (eq? sr '()) (throw 'ret #f)) + (let ((numrem 0) (countrem 0) (newglist '())) + (for-each (lambda (g) + (let ((newlist '())) + (for-each (lambda (o) + (if (< (- (current-time) (cdr (assoc 'recvtime o))) 300) + (set! newlist (cons o newlist)) + (set! countrem (+ countrem 1)))) + (cdr g)) + (if (> (length newlist) 0) + (set! newglist (cons (cons (car g) (sort newlist sr-less?)) newglist)) + (set! numrem (+ numrem 1))))) + sr) + (set! sr (sort newglist srg-less?)) + (if (> countrem 0) + (logf (string-append "removed " (number->string countrem) " time-outed results and " (number->string numrem) " entire sizes")))) + (let ((numrem 0) (newlist '())) + (for-each (lambda (o) + (if (> (- (current-time) (cdr (assoc 'time o))) 1800) + (set! numrem (+ numrem 1)) + (set! newlist (cons o newlist)))) + dpeers) + (set! dpeers newlist) + (logf (string-append "re-enabled " (number->string numrem) " disabled users"))) + (let ((numrem 0) (countrem 0) (newglist '())) + (for-each (lambda (g) + (let ((newlist '())) + (for-each (lambda (o) + (if (not (assoc (cdr (assoc 'peer o)) dpeers)) + (set! newlist (cons o newlist)) + (set! countrem (+ countrem 1)))) + (cdr g)) + (if (> (length newlist) 0) + (set! newglist (cons (cons (car g) (sort newlist sr-less?)) newglist)) + (set! numrem (+ numrem 1))))) + sr) + (set! sr (sort newglist srg-less?)) + (if (> countrem 0) + (logf (string-append "removed " (number->string countrem) " results with disabled peers and " (number->string numrem) " entire sizes")))) + (and (eq? sr '()) (throw 'ret #f)) + (set! numtotal (apply + (map (lambda (o) (length (cdr o))) sr))) + (let* ((maxsize (apply max (map (lambda (o) (length (cdr o))) sr))) + (minsize (/ maxsize 3))) + (let ((numrem 0) (countrem 0)) + (for-each (lambda (o) (if (< (length (cdr o)) minsize) + (begin (set! countrem (+ countrem (length (cdr o)))) + (set! numrem (+ numrem 1))))) + sr) + (if (> countrem 0) + (logf (string-append "will disregard " (number->string countrem) " results from " (number->string numrem) " sizes due to popularity lack"))) + (set! numreal (- numtotal countrem))) + (let ((numrem 0) (numrrem 0)) + (for-each (lambda (g) + (for-each (lambda (o) + (if (< (cdr (assoc 'slots o)) 1) + (begin (set! numrem (+ numrem 1)) + (if (>= (length (cdr g)) minsize) + (set! numrrem (+ numrrem 1)))))) + (cdr g))) + sr) + (if (> numrem 0) + (logf (string-append (number->string numrem) " results had no slots"))) + (set! numavail (- numreal numrrem))) + (for-each (lambda (g) + (if (>= (length (cdr g)) minsize) + (catch 'found + (lambda () + (for-each (lambda (o) + (and (> (cdr (assoc 'slots o)) 0) + (throw 'found o))) + (cdr g))) + (lambda (sig sr) + (let ((tr (gettrbysize (cdr (assoc 'size sr))))) + (if (not tr) + (if (< (length trans) (cdr (assoc 'maxtrans session))) + (download sr)) + (if (and (cdr (assoc 'curspeed tr)) + (not (equal? (cdr (assoc 'peer sr)) (cdr (assoc 'peer tr)))) + (> (- (or (cdr (assoc 'speed sr)) (defspeed sr)) (cdr (assoc 'curspeed tr))) 10000)) + (begin (logf (string-append "abandoning transfer " + (number->string (cdr (assoc 'id tr))) + " for possible faster sender")) + (dc-ecmd-assert 200 "cancel" (cdr (assoc 'id tr))) + (set! trans (assq-remove! trans (cdr (assoc 'id tr)))) + (download sr))))))))) + sr) + ) + ) + (lambda (sig ret) + (set! retval ret) + )) + (set! info-numavail numavail) + (set! info-numreal numreal) + (set! info-numtotal numtotal) + retval) + ) + +(define (handlesr filename fnet peer size slots resptime) + (let ((cl (or (assoc size sr) + (let ((newp (cons size '()))) (set! sr (append sr (list newp))) newp))) + (newsr (list + (cons 'filename filename) + (cons 'peer (list fnet peer)) + (cons 'size size) + (cons 'slots slots) + (cons 'resptime resptime) + (cons 'speed (getspeed peer)) + (cons 'recvtime (current-time)) + (cons 'dis #f))) + (newlist '())) + (for-each (lambda (o) (if (not (and (equal? (cdr (assoc 'filename o)) filename) + (equal? (cdr (assoc 'peer o)) (list fnet peer)))) + (set! newlist (cons o newlist)))) + (cdr cl)) + (set-cdr! cl (sort (cons newsr newlist) sr-less?)) + ) + ) + +; XXX: Redefine to go through the server, once that is implemented +(define (getspeed username) + (catch 'system-error + (lambda () + (let* ((port (open-input-file (string-append (getenv "HOME") "/dc/users/" username))) (avg 0) (numdls (string->number (read-line port))) (max (string->number (read-line port))) (numents (string->number (read-line port)))) + (do ((i 0 (+ i 1))) ((= i numents) (close-port port) (/ avg numents)) (set! avg (+ avg (string->number (read-line port))))) + )) + (lambda args + #f + ) + ) + ) + +(define (validate-session session) + (catch 'wrong-type-arg + (lambda () + (and + (assoc 'sexpr session) + (assoc 'prio session) + (assoc 'maxtrans session) + #t + ) + ) + (lambda (key . args) + (display "Session data is not an a-list\n" (current-error-port)) + #f) + ) + ) + +(define (autodl-main args) + (let ((dc-server #f) (done #f) (retval 0)) + (let ((getopt (make-getopt (cdr args) "hs:S:e:p:t:a:I:")) (arg #f)) + (do ((arg (getopt) (getopt))) ((not (and (pair? arg) (char? (car arg)))) (set! args arg)) + (cond ((eq? (car arg) #\h) + (begin (display "usage: autodl [-s server] -S sessfile\n" (current-error-port)) + (display " autodl [-s server] -e search-expression [-p prio] [-t tag] [-a userarg]\n" (current-error-port)) + (display " autodl [-s server]\n" (current-error-port)) + (display " autodl -h\n" (current-error-port)) + (exit 0))) + ((eq? (car arg) #\s) + (set! dc-server (cdr arg))) + ((eq? (car arg) #\S) + (let ((port (open-file (cdr arg)))) (set! session (read port)) (close-port port))) + ((eq? (car arg) #\p) + (let ((c (assoc 'prio session))) + (if c (set-cdr! c (cdr arg)) + (set! session (cons (cons 'prio (cdr arg)) session))))) + ((eq? (car arg) #\t) + (let ((c (assoc 'tag session))) + (if c (set-cdr! c (cdr arg)) + (set! session (cons (cons 'tag (cdr arg)) session))))) + ((eq? (car arg) #\a) + (let ((c (assoc 'uarg session))) + (if c (set-cdr! c (cdr arg)) + (set! session (cons (cons 'uarg (cdr arg)) session))))) + ((eq? (car arg) #\I) + (let ((c (assoc 'info-file session))) + (if c (set-cdr! c (cdr arg)) + (set! session (cons (cons 'info-file (cdr arg)) session))))) + ((eq? (car arg) #\e) + (set! session (cons (cons 'sexpr (dc-lexsexpr (cdr arg))) session))) + ) + ) + ) + (if (eq? session '()) (begin (if (isatty? (current-input-port)) (display "Enter session data (s-expr):\n" (current-error-port))) (set! session (read)))) + (if (not (assoc 'prio session)) + (set! session (cons '(prio . 10) session))) + (if (not (assoc 'maxtrans session)) + (set! session (cons '(maxtrans . 1) session))) + (if (not (validate-session session)) (begin (display "Invalid session!\n" (current-error-port)) (exit 1))) + (if (not dc-server) (set! dc-server (getenv "DCSERVER"))) + (if (not dc-server) (set! dc-server "localhost")) + (catch 'system-error + (lambda () + (dc-c&l #t dc-server #t)) + (lambda (key . args) + (logf (string-append "could not connect to server: " (apply format #f (cadr args) (caddr args)))) + (exit 2))) + (dc-ecmd-assert 200 "notify" "all" "on") + (for-each (lambda (sig) (sigaction sig (lambda (sig) (throw 'sig sig)))) (list SIGINT SIGTERM SIGHUP)) + (catch 'sig + (lambda () + (while #t + (if (and (not (= lastsearch -1)) (wanttosearch)) + (begin + (if (not (= srchid -1)) + (dc-ecmd "cansrch" srchid)) + (let* ((resp (apply dc-ecmd-assert (append (list '(200 501 509) "search" "prio" (number->string (cdr (assoc 'prio session))) "all") (cdr (assoc 'sexpr session))))) + (ires (dc-intresp resp)) + (eres (dc-extract resp))) + (case (cdr (assoc 'code eres)) + ((200) + (begin (set! srchid (car ires)) + (logf (string-append "search scheduled in " (number->string (cadr ires)) " seconds (id " (number->string srchid) ")")) + (set! info-searcheta (+ (current-time) (cadr ires))) + (set! lastsearch -1))) + ((501) + (begin (set! srchid -1) + (logf (string-append "no fnetnodes available to search on")) + (set! lastsearch (current-time)))) + ((509) + (begin (logf "illegal search expression") + (set! done #t) + (set! retval 3) + (throw 'sig 0))))))) + (checktrans) + (if (> (- (current-time) lastparse) 20) + (begin (parseresults) + (set! lastparse (current-time)))) + (write-info-file) + (dc-select 10000) + (while (let ((resp (dc-getresp))) + (if resp + (begin + (let* ((er (dc-extract resp)) (code (cdr (assoc 'code er))) (cmd (cdr (assoc 'cmd er)))) + (cond + ((equal? cmd ".notify") + (case code + ((611) ; Transfer state change + (let ((ires (dc-intresp resp)) (tr #f)) + (if (and ires (assoc (car ires) trans)) + (begin (set! tr (cdr (assoc (car ires) trans))) + (set-cdr! (assoc 'state tr) + (cdr (assoc (cadr ires) '((0 . wait) (1 . hs) (2 . main) (3 . done))))) + (set-cdr! (assoc 'lastprog tr) (current-time)))))) + ((614) ; Transfer error + (let ((ires (dc-intresp resp))) + (if (and ires (assoc (car ires) trans)) + (begin (logf (string-append "transfer " (number->string (car ires)) " encountered error " (number->string (cadr ires)))) + (dc-ecmd-assert 200 "cancel" (car ires)) + (let ((tr (cdr (assoc (car ires) trans)))) + (disablepeer (cdr (assoc 'peer tr)))) + (set! trans (assq-remove! trans (car ires))))))) + ((615) ; Transfer progress + (let ((ires (dc-intresp resp)) (tr #f)) + (if (and ires (assoc (car ires) trans)) + (begin (set! tr (cdr (assoc (car ires) trans))) + (set-cdr! (assoc 'curpos tr) (cadr ires)) + (set-cdr! (assoc 'lastprog tr) (current-time)))))) + ((617) ; Transfer destroyed + (let* ((ires (dc-intresp resp)) (tr (and ires (assoc (car ires) trans)))) + (if tr + (begin (if (eq? (cdr (assoc 'state (cdr tr))) 'done) + (begin (logf (string-append "transfer " (number->string (car ires)) " done")) + (set! trans (assq-remove! trans (car ires))) + (set! done #t) + (throw 'sig 0)) + (begin (logf (string-append "transfer " (number->string (car ires)) " disappeared")) + (set! trans (assq-remove! trans (car ires))))))))) + ((620) ; Search rescheduled + (let ((ires (dc-intresp resp))) + (if (and ires (= (car ires) srchid)) + (begin (set! info-searcheta (+ (current-time) (cadr ires))) + (logf (string-append "search rescheduled to T+" (number->string (cadr ires)))))))) + ((621) ; Search committed + (let ((ires (dc-intresp resp))) + (if (and ires (= (car ires) srchid)) + (begin (logf "search committed") + (set! info-searcheta 0) + (set! lastsearch (current-time)))))) + ((622) ; Search result + (let ((ires (list->vector (dc-intresp resp)))) + (if (and ires (= (vector-ref ires 0) srchid)) (apply handlesr (map (lambda (n) (vector-ref ires n)) '(1 2 3 4 5 7)))))) + + ) + ) + + ) + ) + #t) + #f) + ) + #t + ) + ) + ) + (lambda (key sig) + (logf (string-append "interrupted by signal " (number->string sig))) + (if (not done) + (set! retval 1))) + ) + (logf "quitting...") + (catch 'sig + (lambda () + (if (dc-connected) + (begin (for-each (lambda (o) + (dc-qcmd (list "cancel" (car o)))) + trans) + (if (assoc 'info-file session) + (catch 'system-error + (lambda () + (delete-file (cdr (assoc 'info-file session)))) + (lambda (key . args) #t))) + (if (and done (assoc 'tag session)) + (dc-qcmd (list "filtercmd" "rmtag" (cdr (assoc 'tag session))))) + (if (not (= srchid -1)) + (dc-qcmd (list "cansrch" srchid))) + (dc-qcmd '("quit")) + (while (dc-connected) (dc-select)) + ))) + (lambda (key sig) + (logf "forcing quit"))) + (exit retval) + ) + ) + +(setlocale LC_ALL "") +(autodl-main (command-line)) diff --git a/lib/guile/chatlog b/lib/guile/chatlog new file mode 100755 index 0000000..0c5b097 --- /dev/null +++ b/lib/guile/chatlog @@ -0,0 +1,149 @@ +#!/usr/bin/guile -s +!# + +(use-modules (dolcon ui)) +(use-modules (ice-9 pretty-print)) + +(define fnetnodes '()) + +(define (make-getopt opts optdesc) + (let ((arg opts) (curpos 0) (rest '())) + (lambda () + (if (eq? arg '()) rest + (let ((ret #f)) + (while (not ret) + (if (= curpos 0) + (if (eq? (string-ref (car arg) 0) #\-) + (set! curpos 1) + (begin + (set! rest (append rest (list (car arg)))) + (set! arg (cdr arg)) + (if (eq? arg '()) + (set! ret #t))))) + (if (> curpos 0) + (if (< curpos (string-length (car arg))) + (begin (set! ret (string-ref (car arg) curpos)) (set! curpos (+ curpos 1))) + (begin (set! curpos 0) (set! arg (cdr arg)) (if (eq? arg '()) (set! ret #t)))))) + (if (eq? ret #t) rest + (let ((opt (string-index optdesc ret))) + (if (eq? opt #f) (throw 'illegal-option ret) + (if (and (< opt (- (string-length optdesc) 1)) (eq? (string-ref optdesc (+ opt 1)) #\:)) + (let ((ret + (cons ret (let ((optarg + (if (< curpos (string-length (car arg))) + (substring (car arg) curpos) + (begin (set! arg (cdr arg)) (if (eq? arg '()) (throw 'requires-argument ret)) (car arg))))) + (set! arg (cdr arg)) optarg)))) + (set! curpos 0) + ret) + (list ret)))))))))) + +(define (fn-getnames) + (let ((resp (dc-ecmd "lsnodes")) (er #f)) + (if (and resp (begin (set! er (dc-extract resp)) er) (= (cdr (assoc 'code er)) 200)) + (let ((ires #f)) + (while (begin (set! ires (dc-intresp resp)) ires) + (if (assoc (car ires) fnetnodes) + (set-cdr! (assoc (car ires) fnetnodes) (caddr ires)) + (set! fnetnodes (cons (cons (car ires) (caddr ires)) fnetnodes)))))))) + +(define (fn-getname id) + (if (not (assoc id fnetnodes)) + (fn-getnames)) + (if (assoc id fnetnodes) + (cdr (assoc id fnetnodes)) + (number->string id))) + +;(define (fn-getname id) +; (let ((resp (dc-ecmd "lsnodes")) (er #f)) +; (if (and resp (begin (set! er (dc-extract resp)) er) (= (cdr (assoc 'code er)) 200)) +; (begin +; (catch 'found +; (lambda () +; (let ((ires #f)) +; (while (begin (set! ires (dc-intresp resp)) ires) +; (if (= (car ires) id) +; (throw 'found (caddr ires))) +; )) +; (number->string id) +; ) +; (lambda (key ret) +; ret))) +; (number->string id))) +; ) + +(define (chatlog-main args) + (let ((dc-server #f) (log-dir #f) (last-fn #f)) + (let ((getopt (make-getopt (cdr args) "hs:S:e:")) (arg #f)) + (do ((arg (getopt) (getopt))) ((not (and (pair? arg) (char? (car arg)))) (set! args arg)) + (cond ((eq? (car arg) #\h) + (begin (display "usage: chatlog [-s server] [-d log-dir]\n" (current-error-port)) + (display " chatlog -h\n" (current-error-port)) + (exit 0))) + ((eq? (car arg) #\s) + (set! dc-server (cdr arg))) + ((eq? (car arg) #\d) + (set! log-dir (cdr arg))) + ) + ) + ) + (if (not dc-server) (set! dc-server (getenv "DCSERVER"))) + (if (not dc-server) (set! dc-server "localhost")) + (if (not log-dir) (set! log-dir (string-append (getenv "HOME") "/dc/chatlog"))) + + (dc-c&l #t dc-server #t) + (dc-ecmd-assert 200 "notify" "fn:chat" "on" "fn:act" "on") + + (while #t + (dc-select 10000) + (while (let ((resp (dc-getresp))) + (if resp + (begin + (let* ((er (dc-extract resp)) (code (cdr (assoc 'code er))) (cmd (cdr (assoc 'cmd er)))) + (cond + ((equal? cmd ".notify") + (case code + ((600) + (let ((ires (list->vector (dc-intresp resp)))) + (if ires + (let ((p (open-file + (string-append log-dir "/" + (let ((fixedname (list->string + (map (lambda (c) (if (eq? c #\/) #\_ c)) + (string->list (fn-getname (vector-ref ires 0))))))) + (if (= (string-length fixedname) 0) "noname" fixedname))) + "a"))) + (if (not (eq? (vector-ref ires 0) last-fn)) + (begin (write-line (string-append " -- " (fn-getname (vector-ref ires 0)) ":")) + (set! last-fn (vector-ref ires 0)))) + (for-each + (lambda (p) + (write-line (string-append (strftime "%H:%M:%S" (localtime (current-time))) ": <" (vector-ref ires 3) "> " (vector-ref ires 4)) p)) + (list p (current-output-port))) + (close-port p)) + )) + ) + ((602) + (let ((ires (dc-intresp resp))) + (if ires + (let ((ent (assoc (car ires) fnetnodes))) + (if ent + (set-cdr! ent (cadr ires)) + (set! fnetnodes (cons (cons (car ires) (cadr ires)) fnetnodes))))))) + + ) + ) + + ) + ) + #t) + #f) + ) + #t + ) + + ) + ) + ) + +(chatlog-main (command-line)) diff --git a/lib/guile/dolcon-guile.c b/lib/guile/dolcon-guile.c new file mode 100644 index 0000000..9e05753 --- /dev/null +++ b/lib/guile/dolcon-guile.c @@ -0,0 +1,339 @@ +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include + +struct respsmob +{ + struct dc_response *resp; +}; + +struct scmcb +{ + SCM subr; +}; + +static int fd = -1; +static scm_bits_t resptype; + +static SCM scm_dc_connect(SCM host, SCM port) +{ + int cport; + + SCM_ASSERT(SCM_STRINGP(host), host, SCM_ARG1, "dc-connect"); + if(port == SCM_UNDEFINED) + { + cport = -1; + } else { + SCM_ASSERT(SCM_INUMP(port), port, SCM_ARG2, "dc-connect"); + cport = SCM_INUM(port); + } + if(fd >= 0) + dc_disconnect(); + if((fd = dc_connect(SCM_STRING_CHARS(host), cport)) < 0) + scm_syserror("dc-connect"); + return(SCM_MAKINUM(fd)); +} + +static SCM scm_dc_disconnect(void) +{ + dc_disconnect(); + return(SCM_MAKINUM(0)); +} + +static SCM scm_dc_connected(void) +{ + return((fd == -1)?SCM_BOOL_F:SCM_BOOL_T); +} + +static SCM scm_dc_select(SCM timeout) +{ + struct pollfd pfd; + int cto, ret, enob; + + if(timeout == SCM_UNDEFINED) + { + cto = -1; + } else { + SCM_ASSERT(SCM_INUMP(timeout), timeout, SCM_ARG1, "dc-select"); + cto = SCM_INUM(timeout); + } + if(fd < 0) + scm_syserror_msg("dc-select", "Not connected", SCM_EOL, ENOTCONN); + pfd.fd = fd; + pfd.events = POLLIN; + if(dc_wantwrite()) + pfd.events |= POLLOUT; + if((ret = poll(&pfd, 1, cto)) < 0) + { + if(errno == EINTR) + return(SCM_BOOL_F); + enob = errno; + dc_disconnect(); + errno = enob; + scm_syserror("dc-select"); + } + if(((pfd.revents & POLLIN) && dc_handleread()) || ((pfd.revents & POLLOUT) && dc_handlewrite())) + { + if(errno == 0) + { + fd = -1; + return(SCM_BOOL_F); + } + scm_syserror("dc-select"); + } + return(ret?SCM_BOOL_T:SCM_BOOL_F); +} + +static SCM makerespsmob(struct dc_response *resp) +{ + struct respsmob *data; + + data = scm_must_malloc(sizeof(*data), "respsmob"); + data->resp = resp; + SCM_RETURN_NEWSMOB(resptype, data); +} + +static SCM scm_dc_getresp(SCM tag) +{ + struct dc_response *resp; + SCM ret; + + if(tag == SCM_UNDEFINED) + { + if((resp = dc_getresp()) == NULL) + return(SCM_BOOL_F); + } else { + SCM_ASSERT(SCM_INUMP(tag), tag, SCM_ARG1, "dc-getresp"); + if((resp = dc_gettaggedresp(SCM_INUM(tag))) == NULL) + return(SCM_BOOL_F); + } + ret = makerespsmob(resp); + return(ret); +} + +static SCM scm_dc_extract(SCM scm_resp) +{ + int i, o; + struct dc_response *resp; + SCM ret, l, w; + + SCM_ASSERT(SCM_SMOB_PREDICATE(resptype, scm_resp), scm_resp, SCM_ARG1, "dc-extract"); + resp = ((struct respsmob *)SCM_SMOB_DATA(scm_resp))->resp; + ret = SCM_EOL; + ret = scm_cons(scm_cons(scm_str2symbol("cmd"), scm_makfrom0str(icswcstombs(resp->cmdname, "UTF-8", NULL))), ret); + ret = scm_cons(scm_cons(scm_str2symbol("code"), SCM_MAKINUM(resp->code)), ret); + ret = scm_cons(scm_cons(scm_str2symbol("tag"), SCM_MAKINUM(resp->tag)), ret); + l = SCM_EOL; + for(i = resp->numlines - 1; i >= 0; i--) + { + w = SCM_EOL; + for(o = resp->rlines[i].argc - 1; o >= 0; o--) + w = scm_cons(scm_makfrom0str(icswcstombs(resp->rlines[i].argv[o], "UTF-8", NULL)), w); + l = scm_cons(w, l); + } + ret = scm_cons(scm_cons(scm_str2symbol("resp"), l), ret); + return(ret); +} + +static SCM scm_dc_intresp(SCM scm_resp) +{ + int i; + struct dc_response *resp; + struct dc_intresp *ires; + SCM ret; + + SCM_ASSERT(SCM_SMOB_PREDICATE(resptype, scm_resp), scm_resp, SCM_ARG1, "dc-intresp"); + resp = ((struct respsmob *)SCM_SMOB_DATA(scm_resp))->resp; + if((ires = dc_interpret(resp)) == NULL) + return(SCM_BOOL_F); + ret = SCM_EOL; + for(i = ires->argc - 1; i >= 0; i--) + { + switch(ires->argv[i].type) + { + case 1: + ret = scm_cons(scm_makfrom0str(icswcstombs(ires->argv[i].val.str, "UTF-8", NULL)), ret); + break; + case 2: + ret = scm_cons(scm_int2num(ires->argv[i].val.num), ret); + break; + case 3: + ret = scm_cons(scm_double2num(ires->argv[i].val.flnum), ret); + break; + } + } + dc_freeires(ires); + return(ret); +} + +static int qcmd_scmcb(struct dc_response *resp) +{ + struct scmcb *scmcb; + + scmcb = resp->data; + scm_apply(scmcb->subr, scm_cons(makerespsmob(resp), SCM_EOL), SCM_EOL); + scm_gc_unprotect_object(scmcb->subr); + free(scmcb); + return(2); +} + +static SCM scm_dc_qcmd(SCM argv, SCM callback) +{ + int tag, enob; + wchar_t **toks, *tok, *cmd; + size_t tokssize, toksdata; + SCM port; + struct scmcb *scmcb; + + SCM_ASSERT(SCM_CONSP(argv), argv, SCM_ARG1, "dc-qcmd"); + if(callback != SCM_UNDEFINED) + SCM_ASSERT(SCM_CLOSUREP(callback), callback, SCM_ARG2, "dc-qcmd"); + cmd = NULL; + toks = NULL; + tokssize = toksdata = 0; + for(; argv != SCM_EOL; argv = SCM_CDR(argv)) + { + port = scm_open_output_string(); + scm_display(SCM_CAR(argv), port); + if((tok = icmbstowcs(SCM_STRING_CHARS(scm_get_output_string(port)), "UTF-8")) == NULL) + { + enob = errno; + addtobuf(toks, NULL); + dc_freewcsarr(toks); + if(cmd != NULL) + free(cmd); + errno = enob; + scm_syserror("dc-qcmd"); + } + if(cmd == NULL) + cmd = tok; + else + addtobuf(toks, tok); + } + addtobuf(toks, NULL); + if(callback == SCM_UNDEFINED) + { + tag = dc_queuecmd(NULL, NULL, cmd, L"%%a", toks, NULL); + } else { + scmcb = scm_must_malloc(sizeof(*scmcb), "scmcb"); + scm_gc_protect_object(scmcb->subr = callback); + tag = dc_queuecmd(qcmd_scmcb, scmcb, cmd, L"%%a", toks, NULL); + } + dc_freewcsarr(toks); + if(cmd != NULL) + free(cmd); + return(SCM_MAKINUM(tag)); +} + +static void login_scmcb(int err, wchar_t *reason, struct scmcb *scmcb) +{ + SCM errsym; + + switch(err) + { + case DC_LOGIN_ERR_SUCCESS: + errsym = scm_str2symbol("success"); + break; + case DC_LOGIN_ERR_NOLOGIN: + errsym = scm_str2symbol("nologin"); + break; + case DC_LOGIN_ERR_SERVER: + errsym = scm_str2symbol("server"); + break; + case DC_LOGIN_ERR_USER: + errsym = scm_str2symbol("user"); + break; + case DC_LOGIN_ERR_CONV: + errsym = scm_str2symbol("conv"); + break; + case DC_LOGIN_ERR_AUTHFAIL: + errsym = scm_str2symbol("authfail"); + break; + } + scm_apply(scmcb->subr, scm_cons(errsym, scm_cons((reason == NULL)?SCM_BOOL_F:scm_makfrom0str(icswcstombs(reason, "UTF-8", NULL)), SCM_EOL)), SCM_EOL); + scm_gc_unprotect_object(scmcb->subr); + free(scmcb); +} + +static SCM scm_dc_loginasync(SCM callback, SCM useauthless, SCM username) +{ + struct scmcb *scmcb; + + SCM_ASSERT(SCM_CLOSUREP(callback), callback, SCM_ARG1, "dc-loginasync"); + scmcb = scm_must_malloc(sizeof(*scmcb), "scmcb"); + scm_gc_protect_object(scmcb->subr = callback); + dc_loginasync(SCM_STRINGP(username)?SCM_STRING_CHARS(username):NULL, SCM_NFALSEP(useauthless), NULL, (void (*)(int, wchar_t *, void *))login_scmcb, scmcb); + return(SCM_BOOL_T); +} + +static SCM scm_dc_lexsexpr(SCM sexpr) +{ + SCM ret; + wchar_t **arr, **ap, *buf; + + SCM_ASSERT(SCM_STRINGP(sexpr), sexpr, SCM_ARG1, "dc-lexsexpr"); + if((buf = icmbstowcs(SCM_STRING_CHARS(sexpr), NULL)) == NULL) + scm_syserror("dc-lexsexpr"); + arr = dc_lexsexpr(buf); + free(buf); + ret = SCM_EOL; + if(arr != NULL) + { + for(ap = arr; *ap != NULL; ap++) + ret = scm_cons(scm_makfrom0str(icswcstombs(*ap, "UTF-8", NULL)), ret); + dc_freewcsarr(arr); + } + return(scm_reverse(ret)); +} + +static size_t resp_free(SCM respsmob) +{ + struct respsmob *data; + + data = (struct respsmob *)SCM_SMOB_DATA(respsmob); + dc_freeresp(data->resp); + free(data); + return(sizeof(*data)); +} + +static int resp_print(SCM respsmob, SCM port, scm_print_state *pstate) +{ + struct respsmob *data; + + data = (struct respsmob *)SCM_SMOB_DATA(respsmob); + scm_puts("#resp->tag), port); + scm_puts(" ", port); + scm_puts(icswcstombs(data->resp->cmdname, "UTF-8", NULL), port); + scm_puts(" ", port); + scm_display(SCM_MAKINUM(data->resp->code), port); + scm_puts(">", port); + return(1); +} + +void init_guiledc(void) +{ + scm_c_define_gsubr("dc-connect", 1, 1, 0, scm_dc_connect); + scm_c_define_gsubr("dc-disconnect", 0, 0, 0, scm_dc_disconnect); + scm_c_define_gsubr("dc-connected", 0, 0, 0, scm_dc_connected); + scm_c_define_gsubr("dc-select", 0, 1, 0, scm_dc_select); + scm_c_define_gsubr("dc-getresp", 0, 1, 0, scm_dc_getresp); + scm_c_define_gsubr("dc-extract", 1, 0, 0, scm_dc_extract); + scm_c_define_gsubr("dc-intresp", 1, 0, 0, scm_dc_intresp); + scm_c_define_gsubr("dc-qcmd", 1, 1, 0, scm_dc_qcmd); + scm_c_define_gsubr("dc-loginasync", 2, 1, 0, scm_dc_loginasync); + scm_c_define_gsubr("dc-lexsexpr", 1, 0, 0, scm_dc_lexsexpr); + resptype = scm_make_smob_type("dc-resp", sizeof(struct respsmob)); + scm_set_smob_free(resptype, resp_free); + scm_set_smob_print(resptype, resp_print); + dc_init(); +} diff --git a/lib/guile/dolcon/CVS/Entries b/lib/guile/dolcon/CVS/Entries new file mode 100644 index 0000000..662f626 --- /dev/null +++ b/lib/guile/dolcon/CVS/Entries @@ -0,0 +1,4 @@ +/Makefile.am/1.1/Sun Sep 19 03:51:39 2004// +/ui.scm/1.8/Fri Jun 10 00:24:54 2005// +/util.scm/1.1/Thu Jun 2 18:05:45 2005// +D diff --git a/lib/guile/dolcon/CVS/Repository b/lib/guile/dolcon/CVS/Repository new file mode 100644 index 0000000..952d17b --- /dev/null +++ b/lib/guile/dolcon/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/lib/guile/dolcon diff --git a/lib/guile/dolcon/CVS/Root b/lib/guile/dolcon/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/lib/guile/dolcon/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/lib/guile/dolcon/Makefile.am b/lib/guile/dolcon/Makefile.am new file mode 100644 index 0000000..4996736 --- /dev/null +++ b/lib/guile/dolcon/Makefile.am @@ -0,0 +1,4 @@ +moduledir=$(datadir)/guile/site/dolcon +module_DATA=ui.scm + +EXTRA_DIST=ui.scm diff --git a/lib/guile/dolcon/ui.scm b/lib/guile/dolcon/ui.scm new file mode 100644 index 0000000..73828df --- /dev/null +++ b/lib/guile/dolcon/ui.scm @@ -0,0 +1,74 @@ +(define-module (dolcon ui)) + +(export dc-connect dc-disconnect dc-connected dc-select dc-getresp dc-extract dc-intresp dc-qcmd dc-loginasync dc-lexsexpr) + +(load-extension "libdolcon-guile" "init_guiledc") + +(define-public dc-login + (lambda (useauthless . username) + (let ((done #f) (errsym #f)) + (dc-loginasync + (lambda (err reason) + (set! errsym err) + (set! done #t)) + useauthless (if (pair? username) (car username) #f)) + (while (not done) (dc-select)) + errsym))) + +(define-public dc-must-connect + (lambda args + (let* ((fd (apply dc-connect args)) (resp (dc-extract (do ((resp (dc-getresp) (dc-getresp))) + ((and resp + (equal? (cdr (assoc 'cmd (dc-extract resp))) ".connect")) + resp) + (dc-select))))) + (if (= (cdr (assoc 'code resp)) 200) + fd + (throw 'bad-return (cdr (assoc 'code resp)) (cadr (assoc 'resp resp))) + ) + ) + ) + ) + +(define-public dc-c&l + (lambda (verbose host useauthless) + (let ((fd -1) (print (lambda (obj) (if verbose (display obj (if (port? verbose) verbose (current-error-port))))))) + (print "connecting...\n") + (set! fd (dc-must-connect host)) + (print "authenticating...\n") + (let ((ret (dc-login useauthless))) + (if (not (eq? ret 'success)) + (throw 'login-failure ret))) + (print "authentication success\n") + fd) + ) + ) + +(define-public dc-ecmd + (lambda args + (let ((tag (dc-qcmd args))) + (do ((resp (dc-getresp tag) (dc-getresp tag))) + (resp resp) + (dc-select)) + ) + ) + ) + +(define-public dc-ecmd-assert + (lambda (code . args) + (let* ((resp (apply dc-ecmd args)) (eresp (dc-extract resp))) + (if (not (if (list? code) + (memq (cdr (assoc 'code eresp)) code) + (= (cdr (assoc 'code eresp)) code))) + (throw 'bad-return (cdr (assoc 'code eresp)) (cadr (assoc 'resp eresp))) + ) + resp + ) + ) + ) + +(define-public dc-intall + (lambda (resp) + (let ((retlist '())) + (do ((ires (dc-intresp resp) (dc-intresp resp))) ((not ires) retlist) + (set! retlist (append retlist (list ires))))))) diff --git a/lib/guile/dolcon/util.scm b/lib/guile/dolcon/util.scm new file mode 100644 index 0000000..c772efc --- /dev/null +++ b/lib/guile/dolcon/util.scm @@ -0,0 +1,40 @@ +(define-module (dolcon util)) +(use-modules (dolcon ui)) + +(define fnetnodes '()) + +(define-public dc-fn-update + (lambda () + (set! fnetnodes + (let ((resp (dc-ecmd "lsnodes")) (er #f)) + (if (and resp (begin (set! er (dc-extract resp)) er) (= (cdr (assq 'code er)) 200)) + (map (lambda (o) + (apply (lambda (id net name users state) + (cons id + (list (cons 'id id) + (cons 'net net) + (cons 'name name) + (cons 'users users) + (cons 'state (list-ref '(syn hs est dead) state))))) + o)) + (dc-intall resp)) + '()))) + fnetnodes)) + +(define-public dc-fn-getattrib + (lambda (id attrib) + (if (not (assq id fnetnodes)) + (dc-fn-update)) + (let ((aform (assq id fnetnodes))) + (if aform + (cdr (assq attrib (cdr aform))) + #f)))) + +(define-public dc-fn-getname + (lambda (id) + (dc-fn-getattrib id 'name))) + +(define-public dc-getfnetnodes + (lambda () + (map (lambda (o) (car o)) + fnetnodes))) diff --git a/lib/initcmds.h b/lib/initcmds.h new file mode 100644 index 0000000..70475c0 --- /dev/null +++ b/lib/initcmds.h @@ -0,0 +1,138 @@ + +/* Do not modify this file - it is autogenerated by makecmds */ + +static void initcmds(void) +{ + struct command *cmd; + + cmd = makecmd(L"lssr"); + addresp(cmd, 200, RESP_STR, RESP_STR, RESP_STR, RESP_INT, RESP_INT, RESP_INT, RESP_FLOAT, RESP_END); + addresp(cmd, 201, RESP_END); + addresp(cmd, 514, RESP_END); + cmd = makecmd(L"lssrch"); + addresp(cmd, 200, RESP_INT, RESP_INT, RESP_INT, RESP_INT, RESP_END); + addresp(cmd, 201, RESP_END); + cmd = makecmd(L"shutdown"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 502, RESP_END); + cmd = makecmd(L"quit"); + addresp(cmd, 200, RESP_END); + cmd = makecmd(L"lsauth"); + addresp(cmd, 200, RESP_STR, RESP_END); + addresp(cmd, 201, RESP_END); + cmd = makecmd(L"login"); + addresp(cmd, 200, RESP_END); /* Success */ + addresp(cmd, 300, RESP_STR, RESP_END); /* Auto (think GSS-API) */ + addresp(cmd, 301, RESP_STR, RESP_END); /* No echo */ + addresp(cmd, 302, RESP_STR, RESP_END); /* Echo */ + addresp(cmd, 303, RESP_STR, RESP_END); /* Info message */ + addresp(cmd, 304, RESP_STR, RESP_END); /* Error message */ + addresp(cmd, 501, RESP_END); + addresp(cmd, 503, RESP_END); /* Already logging in */ + addresp(cmd, 504, RESP_END); /* Charset error */ + addresp(cmd, 505, RESP_DSC, RESP_STR, RESP_END); /* Back-end error */ + addresp(cmd, 506, RESP_END); /* Authentication error */ + addresp(cmd, 508, RESP_END); /* No such authentication mechanism */ + cmd = makecmd(L"pass"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 300, RESP_STR, RESP_END); + addresp(cmd, 301, RESP_STR, RESP_END); + addresp(cmd, 302, RESP_STR, RESP_END); + addresp(cmd, 303, RESP_STR, RESP_END); + addresp(cmd, 304, RESP_STR, RESP_END); + addresp(cmd, 504, RESP_END); + addresp(cmd, 505, RESP_DSC, RESP_STR, RESP_END); + addresp(cmd, 506, RESP_END); + addresp(cmd, 507, RESP_END); /* Data not expected */ + cmd = makecmd(L"cnct"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 504, RESP_END); + addresp(cmd, 509, RESP_END); + addresp(cmd, 511, RESP_END); + cmd = makecmd(L"lsnodes"); + addresp(cmd, 200, RESP_INT, RESP_STR, RESP_STR, RESP_INT, RESP_INT, RESP_END); + addresp(cmd, 201, RESP_END); + addresp(cmd, 502, RESP_END); + cmd = makecmd(L"dcnct"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 510, RESP_END); + cmd = makecmd(L"lspeers"); + addresp(cmd, 200, RESP_STR, RESP_STR, RESP_END); /* Peer ID and nick are standardized, so they can be here -- the rest have to be extracted manually */ + addresp(cmd, 201, RESP_END); + addresp(cmd, 510, RESP_END); + cmd = makecmd(L"lspa"); + addresp(cmd, 200, RESP_STR, RESP_INT, RESP_END); + addresp(cmd, 201, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 510, RESP_END); + cmd = makecmd(L"download"); + addresp(cmd, 200, RESP_INT, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 510, RESP_END); + addresp(cmd, 511, RESP_END); + cmd = makecmd(L"lstrans"); + addresp(cmd, 200, RESP_INT, RESP_INT, RESP_INT, RESP_STR, RESP_STR, RESP_STR, RESP_INT, RESP_INT, RESP_END); + addresp(cmd, 201, RESP_END); + addresp(cmd, 502, RESP_END); + cmd = makecmd(L"cancel"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 512, RESP_END); + cmd = makecmd(L"notify"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 501, RESP_END); + cmd = makecmd(L"sendchat"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 501, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 504, RESP_END); + addresp(cmd, 505, RESP_END); + addresp(cmd, 509, RESP_END); + addresp(cmd, 510, RESP_END); + addresp(cmd, 513, RESP_END); + cmd = makecmd(L"search"); + addresp(cmd, 200, RESP_INT, RESP_INT, RESP_END); + addresp(cmd, 501, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 509, RESP_END); + addresp(cmd, 510, RESP_END); + cmd = makecmd(L"cansrch"); + addresp(cmd, 200, RESP_END); + addresp(cmd, 514, RESP_END); + cmd = makecmd(L"filtercmd"); + addresp(cmd, 200, RESP_STR, RESP_END); + addresp(cmd, 201, RESP_END); + addresp(cmd, 501, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 504, RESP_END); + addresp(cmd, 505, RESP_END); + cmd = makecmd(L"lstrarg"); + addresp(cmd, 200, RESP_STR, RESP_STR, RESP_END); + addresp(cmd, 201, RESP_END); + addresp(cmd, 501, RESP_END); + addresp(cmd, 502, RESP_END); + addresp(cmd, 512, RESP_END); + cmd = makecmd(NULL); /* Nameless notification */ + addresp(cmd, 600, RESP_INT, RESP_INT, RESP_STR, RESP_STR, RESP_STR, RESP_END); /* FN chat */ + addresp(cmd, 601, RESP_INT, RESP_INT, RESP_END); /* FN state change */ + addresp(cmd, 602, RESP_INT, RESP_STR, RESP_END); /* FN name change */ + addresp(cmd, 603, RESP_INT, RESP_END); /* FN destroy */ + addresp(cmd, 604, RESP_INT, RESP_STR, RESP_END); /* FN create */ + addresp(cmd, 605, RESP_INT, RESP_INT, RESP_END); /* FN num peers change */ + addresp(cmd, 610, RESP_INT, RESP_INT, RESP_STR, RESP_STR, RESP_END); /* Transfer create */ + addresp(cmd, 611, RESP_INT, RESP_INT, RESP_END); /* Transfer state change */ + addresp(cmd, 612, RESP_INT, RESP_STR, RESP_END); /* Transfer nick change */ + addresp(cmd, 613, RESP_INT, RESP_INT, RESP_END); /* Transfer size change */ + addresp(cmd, 614, RESP_INT, RESP_INT, RESP_END); /* Transfer error update */ + addresp(cmd, 615, RESP_INT, RESP_INT, RESP_END); /* Transfer progress */ + addresp(cmd, 616, RESP_INT, RESP_STR, RESP_END); /* Transfer path change */ + addresp(cmd, 617, RESP_INT, RESP_END); /* Transfer destroy */ + addresp(cmd, 620, RESP_INT, RESP_INT, RESP_END); + addresp(cmd, 621, RESP_INT, RESP_END); + addresp(cmd, 622, RESP_INT, RESP_STR, RESP_STR, RESP_STR, RESP_INT, RESP_INT, RESP_INT, RESP_FLOAT, RESP_END); + cmd = makecmd(NULL); /* Nameless connection */ + addresp(cmd, 200, RESP_STR, RESP_END); + addresp(cmd, 502, RESP_END); +} diff --git a/lib/makecmds b/lib/makecmds new file mode 100755 index 0000000..750b43d --- /dev/null +++ b/lib/makecmds @@ -0,0 +1,49 @@ +#!/usr/bin/perl + +%keys = + ( + "s" => "RESP_STR", + "d" => "RESP_DSC", + "i" => "RESP_INT", + "f" => "RESP_FLOAT" + ); + +print(" +/* Do not modify this file - it is autogenerated by makecmds */ + +static void initcmds(void) +{ + struct command *cmd; + +"); + +while(<>) +{ + ($comment) = /\s*;\s*(.*)$/; + s/\s*;.*$//; + if(($name) = /^:(\w*)/) + { + if($name) + { + print(" cmd = makecmd(L\"$name\");"); + } else { + print(" cmd = makecmd(NULL);"); + } + if($comment) + { + print("\t/* $comment */"); + } + print("\n"); + } elsif(/^([0-9]{3})\s+/g) { + print(" addresp(cmd, $1"); + print(", " . $keys{$1}) while /\G(\S+)\s+/g; + print(", RESP_END);"); + if($comment) + { + print("\t/* $comment */"); + } + print("\n"); + } +} + +print("}\n"); diff --git a/lib/uicmds b/lib/uicmds new file mode 100644 index 0000000..d59d03d --- /dev/null +++ b/lib/uicmds @@ -0,0 +1,130 @@ +:lssr +200 s s s i i i f +201 +514 +:lssrch +200 i i i i +201 +:shutdown +200 +502 +:quit +200 +:lsauth +200 s +201 +:login +200 ; Success +300 s ; Auto (think GSS-API) +301 s ; No echo +302 s ; Echo +303 s ; Info message +304 s ; Error message +501 +503 ; Already logging in +504 ; Charset error +505 d s ; Back-end error +506 ; Authentication error +508 ; No such authentication mechanism +:pass +200 +300 s +301 s +302 s +303 s +304 s +504 +505 d s +506 +507 ; Data not expected +:cnct +200 +502 +504 +509 +511 +:lsnodes +200 i s s i i +201 +502 +:dcnct +200 +502 +510 +:lspeers +200 s s ; Peer ID and nick are standardized, so they can be here -- the rest have to be extracted manually +201 +510 +:lspa +200 s i +201 +502 +510 +:download +200 i +502 +510 +511 +:lstrans +200 i i i s s s i i +201 +502 +:cancel +200 +502 +512 +:notify +200 +501 +:sendchat +200 +501 +502 +504 +505 +509 +510 +513 +:search +200 i i +501 +502 +509 +510 +:cansrch +200 +514 +:filtercmd +200 s +201 +501 +502 +504 +505 +:lstrarg +200 s s +201 +501 +502 +512 +: ; Nameless notification +600 i i s s s ; FN chat +601 i i ; FN state change +602 i s ; FN name change +603 i ; FN destroy +604 i s ; FN create +605 i i ; FN num peers change +610 i i s s ; Transfer create +611 i i ; Transfer state change +612 i s ; Transfer nick change +613 i i ; Transfer size change +614 i i ; Transfer error update +615 i i ; Transfer progress +616 i s ; Transfer path change +617 i ; Transfer destroy +620 i i +621 i +622 i s s s i i i f +: ; Nameless connection +200 s +502 diff --git a/lib/uilib.c b/lib/uilib.c new file mode 100644 index 0000000..4d440de --- /dev/null +++ b/lib/uilib.c @@ -0,0 +1,1108 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ + +/* + * Note: This code is still ugly, since I copied it almost verbatim + * from the daemon's parser. It would need serious cleanups, but at + * least it works for now. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_RESOLVER +#include +#include +#endif + +#include +#include + +#define RESP_END -1 +#define RESP_DSC 0 +#define RESP_STR 1 +#define RESP_INT 2 +#define RESP_FLOAT 3 + +struct respclass +{ + struct respclass *next; + int code; + int nwords; + int *wordt; +}; + +struct command +{ + struct command *next; + wchar_t *name; + struct respclass *classes; +}; + +struct qcmd +{ + struct qcmd *next; + struct command *cmd; + int tag; + char *buf; + size_t buflen; + int (*callback)(struct dc_response *resp); + void *data; +}; + +void dc_uimisc_disconnected(void); + +/* The first command must be the nameless connect command and second + * the notification command. */ +static struct command *commands = NULL; +static struct qcmd *queue = NULL, *queueend = NULL; +static struct dc_response *respqueue = NULL, *respqueueend = NULL; +static int state = -1; +static int fd = -1; +static iconv_t ichandle; +static int resetreader = 1; +static char *dchostname = NULL; +static struct addrinfo *hostlist = NULL, *curhost = NULL; +static int servport; + +static struct dc_response *makeresp(void) +{ + struct dc_response *new; + + new = smalloc(sizeof(*new)); + new->next = NULL; + new->prev = NULL; + new->code = 0; + new->cmdname = NULL; + new->rlines = NULL; + new->linessize = 0; + new->numlines = 0; + new->curline = 0; + new->data = NULL; + return(new); +} + +static void freeqcmd(struct qcmd *qcmd) +{ + if(qcmd->buf != NULL) + free(qcmd->buf); + free(qcmd); +} + +static void unlinkqueue(void) +{ + struct qcmd *qcmd; + + if((qcmd = queue) == NULL) + return; + queue = qcmd->next; + if(queue == NULL) + queueend = NULL; + freeqcmd(qcmd); +} + +static struct qcmd *makeqcmd(wchar_t *name) +{ + struct qcmd *new; + struct command *cmd; + static int tag = 0; + + if(name == NULL) + { + cmd = commands; + } else { + for(cmd = commands; cmd != NULL; cmd = cmd->next) + { + if((cmd->name != NULL) && !wcscmp(cmd->name, name)) + break; + } + if(cmd == NULL) + return(NULL); + } + new = smalloc(sizeof(*new)); + new->tag = tag++; + new->cmd = cmd; + new->buf = NULL; + new->buflen = 0; + new->callback = NULL; + new->next = NULL; + new->data = NULL; + if(queueend == NULL) + { + queue = queueend = new; + } else { + queueend->next = new; + queueend = new; + } + return(new); +} + +static wchar_t *quoteword(wchar_t *word) +{ + wchar_t *wp, *buf, *bp; + int dq, numbs, numc; + + dq = 0; + numbs = 0; + numc = 0; + if(*word == L'\0') + { + dq = 1; + } else { + for(wp = word; *wp != L'\0'; wp++) + { + if(!dq && iswspace(*wp)) + dq = 1; + if((*wp == L'\\') || (*wp == L'\"')) + numbs++; + numc++; + } + } + if(!dq && !numbs) + return(NULL); + bp = buf = smalloc(sizeof(wchar_t) * (numc + numbs + (dq?2:0) + 1)); + if(dq) + *(bp++) = L'\"'; + for(wp = word; *wp != L'\0'; wp++) + { + if((*wp == L'\\') || (*wp == L'\"')) + *(bp++) = L'\\'; + *(bp++) = *wp; + } + if(dq) + *(bp++) = L'\"'; + *(bp++) = L'\0'; + return(buf); +} + +static struct command *makecmd(wchar_t *name) +{ + struct command *new; + + new = smalloc(sizeof(*new)); + new->name = name; + new->classes = NULL; + new->next = commands; + commands = new; + return(new); +} + +static struct respclass *addresp(struct command *cmd, int code, ...) +{ + struct respclass *new; + int i; + int resps[128]; + va_list args; + + va_start(args, code); + i = 0; + while((resps[i++] = va_arg(args, int)) != RESP_END); + i--; + va_end(args); + new = smalloc(sizeof(*new)); + new->code = code; + new->nwords = i; + if(i > 0) + { + new->wordt = smalloc(sizeof(int) * i); + memcpy(new->wordt, resps, sizeof(int) * i); + } else { + new->wordt = NULL; + } + new->next = cmd->classes; + cmd->classes = new; + return(new); +} + +#include "initcmds.h" + +int dc_init(void) +{ + if((ichandle = iconv_open("wchar_t", "utf-8")) == (iconv_t)-1) + return(-1); + initcmds(); + return(0); +} + +void dc_cleanup(void) +{ + iconv_close(ichandle); +} + +void dc_disconnect(void) +{ + struct dc_response *resp; + + state = -1; + if(fd >= 0) + close(fd); + fd = -1; + while(queue != NULL) + unlinkqueue(); + while((resp = dc_getresp()) != NULL) + dc_freeresp(resp); + dc_uimisc_disconnected(); + if(dchostname != NULL) + free(dchostname); + dchostname = NULL; +} + +void dc_freeresp(struct dc_response *resp) +{ + int i, o; + + for(i = 0; i < resp->numlines; i++) + { + for(o = 0; o < resp->rlines[i].argc; o++) + free(resp->rlines[i].argv[o]); + free(resp->rlines[i].argv); + } + free(resp->rlines); + free(resp); +} + +struct dc_response *dc_getresp(void) +{ + struct dc_response *ret; + + if((ret = respqueue) == NULL) + return(NULL); + respqueue = ret->next; + if(respqueue == NULL) + respqueueend = NULL; + else + respqueue->prev = NULL; + return(ret); +} + +struct dc_response *dc_gettaggedresp(int tag) +{ + struct dc_response *resp; + + for(resp = respqueue; resp != NULL; resp = resp->next) + { + if(resp->tag == tag) + { + if(resp->prev != NULL) + resp->prev->next = resp->next; + if(resp->next != NULL) + resp->next->prev = resp->prev; + if(resp == respqueue) + respqueue = resp->next; + if(resp == respqueueend) + respqueueend = resp->prev; + return(resp); + } + } + return(NULL); +} + +struct dc_response *dc_gettaggedrespsync(int tag) +{ + struct pollfd pfd; + struct dc_response *resp; + + while((resp = dc_gettaggedresp(tag)) == NULL) + { + pfd.fd = fd; + pfd.events = POLLIN; + if(dc_wantwrite()) + pfd.events |= POLLOUT; + if(poll(&pfd, 1, -1) < 0) + return(NULL); + if((pfd.revents & POLLIN) && dc_handleread()) + return(NULL); + if((pfd.revents & POLLOUT) && dc_handlewrite()) + return(NULL); + } + return(resp); +} + +int dc_wantwrite(void) +{ + switch(state) + { + case 1: + if((queue != NULL) && (queue->buflen > 0)) + return(1); + break; + } + return(0); +} + +int dc_getstate(void) +{ + return(state); +} + +int dc_queuecmd(int (*callback)(struct dc_response *), void *data, ...) +{ + struct qcmd *qcmd; + int num, freepart; + va_list al; + char *final; + wchar_t **toks; + wchar_t *buf; + wchar_t *part, *tpart; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + num = 0; + va_start(al, data); + while((part = va_arg(al, wchar_t *)) != NULL) + { + if(!wcscmp(part, L"%%a")) + { + for(toks = va_arg(al, wchar_t **); *toks != NULL; toks++) + { + part = *toks; + freepart = 0; + if((tpart = quoteword(part)) != NULL) + { + freepart = 1; + part = tpart; + } + addtobuf(buf, L' '); + bufcat(buf, part, wcslen(part)); + num++; + if(freepart) + free(part); + } + } else { + if(*part == L'%') + { + /* This demands that all arguments that are passed to the + * function are of equal length, that of an int. I know + * that GCC does that on IA32 platforms, but I do not know + * which other platforms and compilers that it applies + * to. If this breaks your platform, please mail me about + * it. + */ + part = vswprintf2(tpart = (part + 1), al); + for(; *tpart != L'\0'; tpart++) + { + if(*tpart == L'%') + { + if(tpart[1] == L'%') + tpart++; + else + va_arg(al, int); + } + } + freepart = 1; + } else { + freepart = 0; + } + if((tpart = quoteword(part)) != NULL) + { + if(freepart) + free(part); + part = tpart; + freepart = 1; + } + if(num > 0) + addtobuf(buf, L' '); + if(num == 0) + { + if((qcmd = makeqcmd(part)) == NULL) + { + if(freepart) + free(part); + if(buf != NULL) + free(buf); + return(-1); + } else { + qcmd->callback = callback; + qcmd->data = data; + } + } + bufcat(buf, part, wcslen(part)); + num++; + if(freepart) + free(part); + } + } + bufcat(buf, L"\r\n\0", 3); + if((final = icwcstombs(buf, "utf-8")) == NULL) + { + free(buf); + return(-1); + } + va_end(al); + free(buf); + qcmd->buf = final; + qcmd->buflen = strlen(final); + return(qcmd->tag); +} + +int dc_handleread(void) +{ + int ret, done; + char *p1, *p2; + size_t len; + int errnobak; + /* Ewww... this really is soo ugly. I need to clean this up some day. */ + static int pstate = 0; + static char inbuf[128]; + static size_t inbufdata = 0; + static wchar_t *cbuf = NULL; + static size_t cbufsize = 0, cbufdata = 0; + static wchar_t *pptr = NULL; + static wchar_t **argv = NULL; + static int argc = 0, args = 0; + static wchar_t *cw = NULL; + static size_t cwsize = 0, cwdata = 0; + static struct dc_response *curresp = NULL; + static int cont = 0; + static int unlink = 0; + + switch(state) + { + case -1: + return(-1); + case 0: + len = sizeof(ret); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &ret, &len); + if(ret) + { + int newfd; + struct sockaddr_storage addr; + struct sockaddr_in *ipv4; + struct sockaddr_in6 *ipv6; + + for(curhost = curhost->ai_next; curhost != NULL; curhost = curhost->ai_next) + { + if((newfd = socket(curhost->ai_family, curhost->ai_socktype, curhost->ai_protocol)) < 0) + { + errnobak = errno; + dc_disconnect(); + errno = errnobak; + return(-1); + } + dup2(newfd, fd); + close(newfd); + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + memcpy(&addr, curhost->ai_addr, curhost->ai_addrlen); + if(addr.ss_family == AF_INET) + { + ipv4 = (struct sockaddr_in *)&addr; + ipv4->sin_port = htons(servport); + } +#ifdef HAVE_IPV6 + if(addr.ss_family == AF_INET6) + { + ipv6 = (struct sockaddr_in6 *)&addr; + ipv6->sin6_port = htons(servport); + } +#endif + if(connect(fd, (struct sockaddr *)&addr, curhost->ai_addrlen)) + { + if(errno == EINPROGRESS) + return(0); + } else { + break; + } + } + if(curhost == NULL) + { + dc_disconnect(); + errno = ret; + return(-1); + } + } + state = 1; + resetreader = 1; + break; + case 1: + if(resetreader) + { + inbufdata = 0; + cbufdata = 0; + pptr = NULL; + pstate = 0; + resetreader = 0; + cont = 0; + if(curresp != NULL) + dc_freeresp(curresp); + curresp = NULL; + } + if(inbufdata == 128) + inbufdata = 0; + ret = read(fd, inbuf + inbufdata, 128 - inbufdata); + if(ret < 0) + { + if((errno == EAGAIN) || (errno == EINTR)) + return(0); + errnobak = errno; + dc_disconnect(); + errno = errnobak; + return(-1); + } else if(ret == 0) { + dc_disconnect(); + errno = 0; + return(-1); + } + inbufdata += ret; + done = 0; + while(!done) + { + if(cbufsize == cbufdata) + { + if(pptr != NULL) + len = pptr - cbuf; + if((cbuf = realloc(cbuf, sizeof(wchar_t) * (cbufsize += 256))) == NULL) + { + dc_disconnect(); + errno = ENOMEM; + return(-1); + } + if(pptr != NULL) + pptr = cbuf + len; + } + p1 = inbuf; + p2 = (char *)(cbuf + cbufdata); + len = sizeof(wchar_t) * (cbufsize - cbufdata); + ret = iconv(ichandle, &p1, &inbufdata, &p2, &len); + memmove(inbuf, p1, inbufdata); + cbufdata = cbufsize - (len / sizeof(wchar_t)); + if(ret < 0) + { + switch(errno) + { + case EILSEQ: + /* XXX Is this really OK? */ + inbufdata = 0; + done = 1; + break; + case EINVAL: + done = 1; + break; + case E2BIG: + break; + default: + errnobak = errno; + dc_disconnect(); + errno = errnobak; + return(-1); + } + } else { + done = 1; + } + } + if(pptr == NULL) + pptr = cbuf; + done = 0; + while(!done && (pptr - cbuf < cbufdata)) + { + switch(pstate) + { + case 0: + if(iswspace(*pptr)) + { + if(*pptr == L'\r') + { + if(pptr == cbuf + cbufdata - 1) + { + done = 1; + break; + } + if(*(++pptr) == L'\n') + { + if(curresp == NULL) + { + curresp = makeresp(); + if((argc > 0) && ((curresp->code = wcstol(argv[0], NULL, 10)) >= 600)) + { + curresp->cmdname = L".notify"; + curresp->internal = commands->next; + curresp->tag = -1; + unlink = 0; + } else { + if((curresp->cmdname = queue->cmd->name) == NULL) + curresp->cmdname = L".connect"; + curresp->data = queue->data; + curresp->tag = queue->tag; + curresp->internal = (void *)(queue->cmd); + unlink = 1; + } + } + sizebuf(&curresp->rlines, &curresp->linessize, curresp->numlines + 1, sizeof(*(curresp->rlines)), 1); + curresp->rlines[curresp->numlines].argc = argc; + curresp->rlines[curresp->numlines].argv = argv; + argv = NULL; + argc = args = 0; + curresp->numlines++; + if(!cont) + { + if((curresp->code >= 600) || (queue == NULL) || (queue->callback == NULL)) + ret = 0; + else + ret = queue->callback(curresp); + if(ret == 0) + { + if(respqueue == NULL) + { + respqueue = respqueueend = curresp; + } else { + curresp->next = NULL; + curresp->prev = respqueueend; + respqueueend->next = curresp; + respqueueend = curresp; + } + } else if(ret == 1) { + dc_freeresp(curresp); + } + curresp = NULL; + if(unlink) + unlinkqueue(); + } + wmemmove(cbuf, pptr, cbufdata -= (pptr - cbuf)); + pptr = cbuf; + } else { + pptr++; + } + } else { + pptr++; + } + } else { + pstate = 1; + cwdata = 0; + } + break; + case 1: + if(iswspace(*pptr) || ((argc == 0) && (*pptr == L'-'))) + { + if(argc == 0) + { + if(*pptr == L'-') + { + cont = 1; + pptr++; + } else { + cont = 0; + } + } + addtobuf(cw, L'\0'); + sizebuf(&argv, &args, argc + 1, sizeof(*argv), 1); + argv[argc++] = cw; + cw = NULL; + cwsize = cwdata = 0; + pstate = 0; + } else if(*pptr == L'\"') { + pstate = 2; + pptr++; + } else if(*pptr == L'\\') { + if(pptr == cbuf + cbufdata - 1) + { + done = 1; + break; + } + addtobuf(cw, *(++pptr)); + pptr++; + } else { + addtobuf(cw, *(pptr++)); + } + break; + case 2: + if(*pptr == L'\"') + { + pstate = 1; + } else if(*pptr == L'\\') { + addtobuf(cw, *(++pptr)); + } else { + addtobuf(cw, *pptr); + } + pptr++; + break; + } + } + break; + } + return(0); +} + +int dc_handlewrite(void) +{ + int ret; + int errnobak; + + switch(state) + { + case 1: + if(queue->buflen > 0) + { + ret = send(fd, queue->buf, queue->buflen, MSG_NOSIGNAL | MSG_DONTWAIT); + if(ret < 0) + { + if((errno == EAGAIN) || (errno == EINTR)) + return(0); + errnobak = errno; + dc_disconnect(); + errno = errnobak; + return(-1); + } + if(ret > 0) + memmove(queue->buf, queue->buf + ret, queue->buflen -= ret); + } + break; + } + return(0); +} + +#ifdef HAVE_RESOLVER +static char *readname(unsigned char *msg, unsigned char *eom, unsigned char **p) +{ + char *name, *tname; + unsigned char *tp; + size_t namesize, namedata, len; + + name = NULL; + namesize = namedata = 0; + while(1) + { + len = *((*p)++); + if(len == 0) + { + addtobuf(name, 0); + return(name); + } else if(len == 0xc0) { + tp = msg + *((*p)++); + if((tname = readname(msg, eom, &tp)) == NULL) + { + if(name != NULL) + free(name); + return(NULL); + } + bufcat(name, tname, strlen(tname)); + addtobuf(name, 0); + free(tname); + return(name); + } else if(*p + len >= eom) { + if(name != NULL) + free(name); + return(NULL); + } + bufcat(name, *p, len); + *p += len; + addtobuf(name, '.'); + } +} + +static int skipname(unsigned char *msg, unsigned char *eom, unsigned char **p) +{ + size_t len; + + while(1) + { + len = *((*p)++); + if(len == 0) + { + return(0); + } else if(len == 0xc0) { + (*p)++; + return(0); + } else if(*p + len >= eom) { + return(-1); + } + *p += len; + } +} + +static int getsrvrr(char *name, char **host, int *port) +{ + int i; + char *name2, *rrname; + unsigned char *eom, *p; + unsigned char buf[1024]; + int flags, num, class, type; + size_t len; + int ret; + + if(!(_res.options & RES_INIT)) + { + if(res_init() < 0) + return(-1); + } + /* res_querydomain doesn't work for some reason */ + name2 = smalloc(strlen("_dolcon._tcp.") + strlen(name) + 2); + strcpy(name2, "_dolcon._tcp."); + strcat(name2, name); + len = strlen(name2); + if(name2[len - 1] != '.') + { + name2[len] = '.'; + name2[len + 1] = 0; + } + ret = res_query(name2, C_IN, T_SRV, buf, sizeof(buf)); + if(ret < 0) + { + free(name2); + return(-1); + } + eom = buf + ret; + flags = (buf[2] << 8) + buf[3]; + if((flags & 0xfa0f) != 0x8000) + { + free(name2); + return(-1); + } + num = (buf[4] << 8) + buf[5]; + p = buf + 12; + for(i = 0; i < num; i++) + { + if(skipname(buf, eom, &p)) + { + free(name2); + return(-1); + } + p += 4; + } + num = (buf[6] << 8) + buf[7]; + for(i = 0; i < num; i++) + { + if((rrname = readname(buf, eom, &p)) == NULL) + { + free(name2); + return(-1); + } + type = *(p++) << 8; + type += *(p++); + class = *(p++) << 8; + class += *(p++); + p += 4; + len = *(p++) << 8; + len += *(p++); + if((class == C_IN) && (type == T_SRV) && !strcmp(rrname, name2)) + { + free(rrname); + free(name2); + /* Noone will want to have alternative DC servers, so + * don't care about priority and weigth */ + p += 4; + if(port == NULL) + { + p += 2; + } else { + *port = *(p++) << 8; + *port += *(p++); + } + if(host != NULL) + { + if((rrname = readname(buf, eom, &p)) == NULL) + return(-1); + *host = rrname; + } + return(0); + } + p += len; + free(rrname); + } + free(name2); + return(-1); +} +#else +static int getsrvrr(char *name, char **host, int *port) +{ + errno = EOPNOTSUP; + return(-1); +} +#endif + +int dc_connect(char *host, int port) +{ + struct addrinfo hint; + struct sockaddr_storage addr; + struct sockaddr_in *ipv4; +#ifdef HAVE_IPV6 + struct sockaddr_in6 *ipv6; +#endif + struct qcmd *qcmd; + char *newhost; + int getsrv, freehost; + int errnobak; + + if(fd >= 0) + dc_disconnect(); + state = -1; + freehost = 0; + if(port < 0) + { + port = 1500; + getsrv = 1; + } else { + getsrv = 0; + } + memset(&hint, 0, sizeof(hint)); + hint.ai_socktype = SOCK_STREAM; + if(getsrv) + { + if(!getsrvrr(host, &newhost, &port)) + { + host = newhost; + freehost = 1; + } + } + servport = port; + if(hostlist != NULL) + freeaddrinfo(hostlist); + if(getaddrinfo(host, NULL, &hint, &hostlist)) + { + errno = ENONET; + if(freehost) + free(host); + return(-1); + } + for(curhost = hostlist; curhost != NULL; curhost = curhost->ai_next) + { + if((fd = socket(curhost->ai_family, curhost->ai_socktype, curhost->ai_protocol)) < 0) + { + errnobak = errno; + if(freehost) + free(host); + errno = errnobak; + return(-1); + } + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + memcpy(&addr, curhost->ai_addr, curhost->ai_addrlen); + if(addr.ss_family == AF_INET) + { + ipv4 = (struct sockaddr_in *)&addr; + ipv4->sin_port = htons(port); + } +#ifdef HAVE_IPV6 + if(addr.ss_family == AF_INET6) + { + ipv6 = (struct sockaddr_in6 *)&addr; + ipv6->sin6_port = htons(port); + } +#endif + if(connect(fd, (struct sockaddr *)&addr, curhost->ai_addrlen)) + { + if(errno == EINPROGRESS) + { + state = 0; + break; + } + close(fd); + fd = -1; + } else { + state = 1; + break; + } + } + qcmd = makeqcmd(NULL); + resetreader = 1; + if(dchostname != NULL) + free(dchostname); + dchostname = sstrdup(host); + if(freehost) + free(host); + return(fd); +} + +struct dc_intresp *dc_interpret(struct dc_response *resp) +{ + int i; + struct dc_intresp *iresp; + struct command *cmd; + struct respclass *cls; + int code; + int args; + + if((resp->numlines == 0) || (resp->rlines[0].argc == 0) || (resp->curline >= resp->numlines)) + return(NULL); + code = wcstol(resp->rlines[0].argv[0], NULL, 10); + cmd = (struct command *)(resp->internal); + for(cls = cmd->classes; cls != NULL; cls = cls->next) + { + if(cls->code == code) + break; + } + if(cls == NULL) + return(NULL); + if(cls->nwords >= resp->rlines[resp->curline].argc) + return(NULL); + iresp = smalloc(sizeof(*iresp)); + iresp->code = code; + iresp->argv = NULL; + iresp->argc = 0; + args = 0; + for(i = 0; i < cls->nwords; i++) + { + switch(cls->wordt[i]) + { + case RESP_DSC: + break; + case RESP_STR: + sizebuf(&(iresp->argv), &args, iresp->argc + 1, sizeof(*(iresp->argv)), 1); + iresp->argv[iresp->argc].val.str = swcsdup(resp->rlines[resp->curline].argv[i + 1]); + iresp->argv[iresp->argc].type = cls->wordt[i]; + iresp->argc++; + break; + case RESP_INT: + sizebuf(&(iresp->argv), &args, iresp->argc + 1, sizeof(*(iresp->argv)), 1); + iresp->argv[iresp->argc].val.num = wcstol(resp->rlines[resp->curline].argv[i + 1], NULL, 0); + iresp->argv[iresp->argc].type = cls->wordt[i]; + iresp->argc++; + break; + case RESP_FLOAT: + sizebuf(&(iresp->argv), &args, iresp->argc + 1, sizeof(*(iresp->argv)), 1); + iresp->argv[iresp->argc].val.flnum = wcstod(resp->rlines[resp->curline].argv[i + 1], NULL); + iresp->argv[iresp->argc].type = cls->wordt[i]; + iresp->argc++; + break; + } + } + resp->curline++; + return(iresp); +} + +void dc_freeires(struct dc_intresp *ires) +{ + int i; + + for(i = 0; i < ires->argc; i++) + { + if(ires->argv[i].type == RESP_STR) + free(ires->argv[i].val.str); + } + free(ires->argv); + free(ires); +} + +const char *dc_gethostname(void) +{ + return(dchostname); +} diff --git a/lib/uilib.h b/lib/uilib.h new file mode 100644 index 0000000..7c0efe4 --- /dev/null +++ b/lib/uilib.h @@ -0,0 +1,23 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ + +#ifndef _UILIB_H +#define _UILIB_H + +#endif diff --git a/lib/uimisc.c b/lib/uimisc.c new file mode 100644 index 0000000..6a759be --- /dev/null +++ b/lib/uimisc.c @@ -0,0 +1,1055 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include +#include +#include + +#ifdef HAVE_KRB5 +#include +#include +#endif + +struct logindata; + +struct authmech +{ + wchar_t *name; + void (*process)(struct dc_response *resp, struct logindata *data); + int (*init)(struct logindata *data); + void (*release)(struct logindata *data); +}; + +struct logindata +{ + int (*conv)(int type, wchar_t *text, char **resp, void *data); + void (*callback)(int err, wchar_t *reason, void *data); + char *username; + int freeusername; + int useauthless; + void *data; + void *mechdata; + struct authmech *mech; +}; + +struct gencbdata +{ + void (*callback)(int resp, void *data); + void *data; +}; + +struct dc_fnetnode *dc_fnetnodes = NULL; +struct dc_transfer *dc_transfers = NULL; + +static void freelogindata(struct logindata *data) +{ + if((data->mech != NULL) && (data->mech->release != NULL)) + data->mech->release(data); + if(data->freeusername) + free(data->username); + free(data); +} + +static int logincallback(struct dc_response *resp); + +static void process_authless(struct dc_response *resp, struct logindata *data) +{ + switch(resp->code) + { + case 200: + data->callback(DC_LOGIN_ERR_SUCCESS, NULL, data->data); + freelogindata(data); + break; +/* + case 303: + case 304: + if((ires = dc_interpret(resp)) != NULL) + { + buf = NULL; + if(data->conv((resp->code == 303)?DC_LOGIN_CONV_INFO:DC_LOGIN_CONV_ERROR, ires->argv[0].val.str, &buf, data->data)) + { + data->callback(DC_LOGIN_ERR_CONV, NULL, data->data); + freelogindata(data); + } else { + dc_queuecmd(logincallback, data, L"pass", L"", NULL); + } + if(buf != NULL) + { + memset(buf, 0, strlen(buf)); + free(buf); + } + dc_freeires(ires); + } + break; +*/ + case 505: + data->callback(DC_LOGIN_ERR_SERVER, NULL, data->data); + freelogindata(data); + break; + case 506: + data->callback(DC_LOGIN_ERR_AUTHFAIL, NULL, data->data); + freelogindata(data); + break; + default: + data->callback(DC_LOGIN_ERR_USER, NULL, data->data); + freelogindata(data); + break; + } +} + +static void process_pam(struct dc_response *resp, struct logindata *data) +{ + struct dc_intresp *ires; + int convtype; + char *buf; + + switch(resp->code) + { + case 200: + data->callback(DC_LOGIN_ERR_SUCCESS, NULL, data->data); + freelogindata(data); + break; + case 301: + case 302: + case 303: + case 304: + if(resp->code == 301) + convtype = DC_LOGIN_CONV_NOECHO; + else if(resp->code == 302) + convtype = DC_LOGIN_CONV_ECHO; + else if(resp->code == 303) + convtype = DC_LOGIN_CONV_INFO; + else if(resp->code == 304) + convtype = DC_LOGIN_CONV_ERROR; + if((ires = dc_interpret(resp)) != NULL) + { + buf = NULL; + if(data->conv(convtype, ires->argv[0].val.str, &buf, data->data)) + { + data->callback(DC_LOGIN_ERR_CONV, NULL, data->data); + freelogindata(data); + } else { + dc_queuecmd(logincallback, data, L"pass", L"%%s", buf, NULL); + } + if(buf != NULL) + { + memset(buf, 0, strlen(buf)); + free(buf); + } + dc_freeires(ires); + } + break; + case 505: + data->callback(DC_LOGIN_ERR_SERVER, NULL, data->data); + freelogindata(data); + break; + case 506: + data->callback(DC_LOGIN_ERR_AUTHFAIL, NULL, data->data); + freelogindata(data); + break; + default: + data->callback(DC_LOGIN_ERR_USER, NULL, data->data); + freelogindata(data); + break; + } +} + +#ifdef HAVE_KRB5 +struct krb5data +{ + int state; + krb5_context context; + krb5_principal sprinc, myprinc; + krb5_ccache ccache; + krb5_auth_context authcon; + krb5_data reqbuf; + krb5_creds *servcreds; + int valid, fwd, fwded; +}; + +static char *hexencode(char *data, size_t datalen) +{ + char *buf, this; + size_t bufsize, bufdata; + int dig; + + buf = NULL; + bufsize = bufdata = 0; + for(; datalen > 0; datalen--, data++) + { + dig = (*data & 0xF0) >> 4; + if(dig > 9) + this = 'A' + dig - 10; + else + this = dig + '0'; + addtobuf(buf, this); + dig = *data & 0x0F; + if(dig > 9) + this = 'A' + dig - 10; + else + this = dig + '0'; + addtobuf(buf, this); + } + addtobuf(buf, 0); + return(buf); +} + +static char *hexdecode(char *data, size_t *len) +{ + char *buf, this; + size_t bufsize, bufdata; + + buf = NULL; + bufsize = bufdata = 0; + for(; *data; data++) + { + if((*data >= 'A') && (*data <= 'F')) + { + this = (this & 0x0F) | ((*data - 'A' + 10) << 4); + } else if((*data >= '0') && (*data <= '9')) { + this = (this & 0x0F) | ((*data - '0') << 4); + } else { + if(buf != NULL) + free(buf); + return(NULL); + } + data++; + if(!*data) + { + if(buf != NULL) + free(buf); + return(NULL); + } + if((*data >= 'A') && (*data <= 'F')) + { + this = (this & 0xF0) | (*data - 'A' + 10); + } else if((*data >= '0') && (*data <= '9')) { + this = (this & 0xF0) | (*data - '0'); + } else { + if(buf != NULL) + free(buf); + return(NULL); + } + addtobuf(buf, this); + } + addtobuf(buf, 0); + if(len != NULL) + *len = bufdata - 1; + return(buf); +} + +static void process_krb5(struct dc_response *resp, struct logindata *data) +{ + int ret; + struct dc_intresp *ires; + struct krb5data *krb; + krb5_data k5d; + krb5_ap_rep_enc_part *repl; + char *buf; + + krb = data->mechdata; + switch(resp->code) + { + case 200: + data->callback(DC_LOGIN_ERR_SUCCESS, NULL, data->data); + freelogindata(data); + break; + case 300: + switch(krb->state) + { + case 0: + buf = hexencode(krb->reqbuf.data, krb->reqbuf.length); + dc_queuecmd(logincallback, data, L"pass", L"%%s", buf, NULL); + free(buf); + krb->state = 1; + break; + case 1: + if((ires = dc_interpret(resp)) != NULL) + { + k5d.data = hexdecode(icswcstombs(ires->argv[0].val.str, NULL, NULL), &k5d.length); + if(!krb->valid) + { + if((ret = krb5_rd_rep(krb->context, krb->authcon, &k5d, &repl)) != 0) + { + data->callback(DC_LOGIN_ERR_SERVER, NULL, data->data); + freelogindata(data); + break; + } + /* XXX: Do I need to do something with this? */ + krb->valid = 1; + krb5_free_ap_rep_enc_part(krb->context, repl); + } + if(krb->fwd && !krb->fwded) + { + if(krb->reqbuf.data != NULL) + free(krb->reqbuf.data); + krb->reqbuf.data = NULL; + if((ret = krb5_fwd_tgt_creds(krb->context, krb->authcon, NULL, krb->servcreds->client, krb->servcreds->server, 0, 1, &krb->reqbuf)) != 0) + { + fprintf(stderr, "krb5_fwd_tgt_creds reported an error: %s\n", error_message(ret)); + dc_queuecmd(logincallback, data, L"pass", L"31", NULL); + krb->fwd = 0; + krb->state = 2; + } else { + dc_queuecmd(logincallback, data, L"pass", L"32", NULL); + krb->state = 0; + krb->fwded = 1; + } + } else { + dc_queuecmd(logincallback, data, L"pass", L"31", NULL); + krb->state = 2; + } + free(k5d.data); + dc_freeires(ires); + } + break; + default: + data->callback(DC_LOGIN_ERR_USER, NULL, data->data); + freelogindata(data); + break; + } + break; + case 505: + data->callback(DC_LOGIN_ERR_SERVER, NULL, data->data); + freelogindata(data); + break; + case 506: + data->callback(DC_LOGIN_ERR_AUTHFAIL, NULL, data->data); + freelogindata(data); + break; + default: + data->callback(DC_LOGIN_ERR_USER, NULL, data->data); + freelogindata(data); + break; + } +} + +static int init_krb5(struct logindata *data) +{ + int ret; + struct krb5data *krb; + krb5_data cksum; + krb5_creds creds; + + krb = smalloc(sizeof(*krb)); + memset(krb, 0, sizeof(*krb)); + krb->fwd = 1; + krb->fwded = 0; + data->mechdata = krb; + if((ret = krb5_init_context(&krb->context)) != 0) + { + fprintf(stderr, "krb5_init_context reported an error: %s\n", error_message(ret)); + return(1); + } + if((ret = krb5_auth_con_init(krb->context, &krb->authcon)) != 0) + { + fprintf(stderr, "krb5_auth_con_init reported an error: %s\n", error_message(ret)); + return(1); + } + krb5_auth_con_setflags(krb->context, krb->authcon, KRB5_AUTH_CONTEXT_DO_SEQUENCE); + if((ret = krb5_sname_to_principal(krb->context, dc_gethostname(), "doldacond", KRB5_NT_SRV_HST, &krb->sprinc)) != 0) + { + fprintf(stderr, "krb5_sname_to_principal reported an error: %s\n", error_message(ret)); + return(1); + } + if((ret = krb5_cc_default(krb->context, &krb->ccache)) != 0) + { + fprintf(stderr, "krb5_cc_default reported an error: %s\n", error_message(ret)); + return(1); + } + if((ret = krb5_cc_get_principal(krb->context, krb->ccache, &krb->myprinc)) != 0) + { + fprintf(stderr, "krb5_cc_default reported an error: %s\n", error_message(ret)); + return(1); + } + memset(&creds, 0, sizeof(creds)); + creds.client = krb->myprinc; + creds.server = krb->sprinc; + if((ret = krb5_get_credentials(krb->context, 0, krb->ccache, &creds, &krb->servcreds)) != 0) + { + fprintf(stderr, "krb5_get_credentials reported an error: %s\n", error_message(ret)); + return(1); + } + /* WTF is this checksum stuff?! The Krb docs don't say a word about it! */ + cksum.data = sstrdup(dc_gethostname()); + cksum.length = strlen(cksum.data); + if((ret = krb5_mk_req_extended(krb->context, &krb->authcon, AP_OPTS_MUTUAL_REQUIRED, &cksum, krb->servcreds, &krb->reqbuf)) != 0) + { + fprintf(stderr, "krb5_mk_req_extended reported an error: %s\n", error_message(ret)); + return(1); + } + free(cksum.data); + krb->state = 0; + return(0); +} + +static void release_krb5(struct logindata *data) +{ + struct krb5data *krb; + + if((krb = data->mechdata) == NULL) + return; + if(krb->servcreds != NULL) + krb5_free_creds(krb->context, krb->servcreds); + if(krb->reqbuf.data != NULL) + free(krb->reqbuf.data); + if(krb->sprinc != NULL) + krb5_free_principal(krb->context, krb->sprinc); + if(krb->myprinc != NULL) + krb5_free_principal(krb->context, krb->myprinc); + if(krb->ccache != NULL) + krb5_cc_close(krb->context, krb->ccache); + if(krb->authcon != NULL) + krb5_auth_con_free(krb->context, krb->authcon); + if(krb->context != NULL) + krb5_free_context(krb->context); + free(krb); +} +#endif + +/* Arranged in order of priority */ +static struct authmech authmechs[] = +{ +#ifdef HAVE_KRB5 + { + .name = L"krb5", + .process = process_krb5, + .init = init_krb5, + .release = release_krb5 + }, +#endif + { + .name = L"authless", + .process = process_authless, + .init = NULL, + .release = NULL + }, + { + .name = L"pam", + .process = process_pam, + .init = NULL, + .release = NULL + }, + { + .name = NULL + } +}; + +static int builtinconv(int type, wchar_t *text, char **resp, void *data) +{ + char *buf, *pass; + + if(isatty(0)) + { + if((buf = icwcstombs(text, NULL)) == NULL) + return(1); + pass = getpass(buf); + free(buf); + *resp = sstrdup(pass); + memset(pass, 0, strlen(pass)); + return(0); + } + return(1); +} + +static int logincallback(struct dc_response *resp) +{ + int i; + struct dc_intresp *ires; + struct logindata *data; + int mech; + char *username; + struct passwd *pwent; + void *odata, *ndata; + + data = resp->data; + if(!wcscmp(resp->cmdname, L"lsauth")) + { + if(resp->code == 201) + { + data->callback(DC_LOGIN_ERR_NOLOGIN, NULL, data->data); + freelogindata(data); + } else { + mech = -1; + while((ires = dc_interpret(resp)) != NULL) + { + if(!data->useauthless && !wcscmp(ires->argv[0].val.str, L"authless")) + { + dc_freeires(ires); + continue; + } + for(i = 0; authmechs[i].name != NULL; i++) + { + if(!wcscmp(authmechs[i].name, ires->argv[0].val.str) && ((i < mech) || (mech == -1))) + { + odata = data->mechdata; + data->mechdata = NULL; + if((authmechs[i].init != NULL) && authmechs[i].init(data)) + { + if(authmechs[i].release != NULL) + authmechs[i].release(data); + data->mechdata = odata; + fprintf(stderr, "authentication mechanism %ls failed, trying further...\n", authmechs[i].name); + } else { + if((data->mech != NULL) && data->mech->release != NULL) + { + ndata = data->mechdata; + data->mechdata = odata; + data->mech->release(data); + data->mechdata = ndata; + } + mech = i; + data->mech = authmechs + i; + } + break; + } + } + dc_freeires(ires); + } + if(mech == -1) + { + data->callback(DC_LOGIN_ERR_NOLOGIN, NULL, data->data); + freelogindata(data); + } else { + if((username = data->username) == NULL) + { + if((pwent = getpwuid(getuid())) == NULL) + { + data->callback(DC_LOGIN_ERR_USER, NULL, data->data); + freelogindata(data); + return(1); + } + username = pwent->pw_name; + } + dc_queuecmd(logincallback, data, L"login", data->mech->name, L"%%s", username, NULL); + } + } + } else if(!wcscmp(resp->cmdname, L"login") || !wcscmp(resp->cmdname, L"pass")) { + data->mech->process(resp, data); + } + return(1); +} + +void dc_loginasync(char *username, int useauthless, int (*conv)(int, wchar_t *, char **, void *), void (*callback)(int, wchar_t *, void *), void *udata) +{ + struct logindata *data; + + data = smalloc(sizeof(*data)); + if(conv == NULL) + conv = builtinconv; + data->conv = conv; + data->mech = NULL; + data->data = udata; + data->mechdata = NULL; + data->callback = callback; + data->useauthless = useauthless; + data->freeusername = 0; + if(username == NULL) + { + data->username = NULL; + } else { + data->username = sstrdup(username); + data->freeusername = 1; + } + dc_queuecmd(logincallback, data, L"lsauth", NULL); +} + +static struct dc_fnetnode *newfn(void) +{ + struct dc_fnetnode *fn; + + fn = smalloc(sizeof(*fn)); + memset(fn, 0, sizeof(*fn)); + fn->id = -1; + fn->name = NULL; + fn->fnet = NULL; + fn->state = fn->numusers = fn->found = 0; + fn->destroycb = NULL; + fn->udata = NULL; + fn->next = dc_fnetnodes; + fn->prev = NULL; + if(dc_fnetnodes != NULL) + dc_fnetnodes->prev = fn; + dc_fnetnodes = fn; + return(fn); +} + +static void freefn(struct dc_fnetnode *fn) +{ + if(fn->next != NULL) + fn->next->prev = fn->prev; + if(fn->prev != NULL) + fn->prev->next = fn->next; + if(fn == dc_fnetnodes) + dc_fnetnodes = fn->next; + if(fn->destroycb != NULL) + fn->destroycb(fn); + if(fn->name != NULL) + free(fn->name); + if(fn->fnet != NULL) + free(fn->fnet); + free(fn); +} + +struct dc_fnetnode *dc_findfnetnode(int id) +{ + struct dc_fnetnode *fn; + + for(fn = dc_fnetnodes; fn != NULL; fn = fn->next) + { + if(fn->id == id) + break; + } + return(fn); +} + +static struct dc_transfer *newtransfer(void) +{ + struct dc_transfer *transfer; + + transfer = smalloc(sizeof(*transfer)); + memset(transfer, 0, sizeof(*transfer)); + transfer->id = -1; + transfer->peerid = transfer->peernick = transfer->path = NULL; + transfer->state = DC_TRNS_WAITING; + transfer->dir = DC_TRNSD_UNKNOWN; + transfer->size = -1; + transfer->curpos = -1; + transfer->destroycb = NULL; + transfer->udata = NULL; + transfer->next = dc_transfers; + transfer->prev = NULL; + if(dc_transfers != NULL) + dc_transfers->prev = transfer; + dc_transfers = transfer; + return(transfer); +} + +static void freetransfer(struct dc_transfer *transfer) +{ + if(transfer->next != NULL) + transfer->next->prev = transfer->prev; + if(transfer->prev != NULL) + transfer->prev->next = transfer->next; + if(transfer == dc_transfers) + dc_transfers = transfer->next; + if(transfer->destroycb != NULL) + transfer->destroycb(transfer); + if(transfer->peerid != NULL) + free(transfer->peerid); + if(transfer->peernick != NULL) + free(transfer->peernick); + if(transfer->path != NULL) + free(transfer->path); + free(transfer); +} + +struct dc_transfer *dc_findtransfer(int id) +{ + struct dc_transfer *transfer; + + for(transfer = dc_transfers; transfer != NULL; transfer = transfer->next) + { + if(transfer->id == id) + break; + } + return(transfer); +} + +static int getfnlistcallback(struct dc_response *resp) +{ + struct dc_intresp *ires; + struct gencbdata *data; + struct dc_fnetnode *fn, *next; + + data = resp->data; + if(resp->code == 200) + { + for(fn = dc_fnetnodes; fn != NULL; fn = fn->next) + fn->found = 0; + while((ires = dc_interpret(resp)) != NULL) + { + if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL) + { + fn->found = 1; + if(fn->fnet != NULL) + free(fn->fnet); + fn->fnet = swcsdup(ires->argv[1].val.str); + if(fn->name != NULL) + free(fn->name); + fn->name = swcsdup(ires->argv[2].val.str); + fn->numusers = ires->argv[3].val.num; + fn->state = ires->argv[4].val.num; + } else { + fn = newfn(); + fn->id = ires->argv[0].val.num; + fn->fnet = swcsdup(ires->argv[1].val.str); + fn->name = swcsdup(ires->argv[2].val.str); + fn->numusers = ires->argv[3].val.num; + fn->state = ires->argv[4].val.num; + fn->found = 1; + } + dc_freeires(ires); + } + for(fn = dc_fnetnodes; fn != NULL; fn = next) + { + next = fn->next; + if(!fn->found) + freefn(fn); + } + data->callback(200, data->data); + free(resp->data); + } else if(resp->code == 201) { + while(dc_fnetnodes != NULL) + freefn(dc_fnetnodes); + data->callback(201, data->data); + free(resp->data); + } else if(resp->code == 502) { + while(dc_fnetnodes != NULL) + freefn(dc_fnetnodes); + data->callback(502, data->data); + free(resp->data); + } + return(1); +} + +static int gettrlistcallback(struct dc_response *resp) +{ + struct dc_intresp *ires; + struct gencbdata *data; + struct dc_transfer *transfer, *next; + + data = resp->data; + if(resp->code == 200) + { + for(transfer = dc_transfers; transfer != NULL; transfer = transfer->next) + transfer->found = 0; + while((ires = dc_interpret(resp)) != NULL) + { + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + transfer->found = 1; + if((transfer->path == NULL) || wcscmp(transfer->path, ires->argv[5].val.str)) + { + if(transfer->path != NULL) + free(transfer->path); + transfer->path = swcsdup(ires->argv[5].val.str); + } + if((transfer->peerid == NULL) || wcscmp(transfer->peerid, ires->argv[3].val.str)) + { + if(transfer->peerid != NULL) + free(transfer->peerid); + transfer->peerid = swcsdup(ires->argv[3].val.str); + } + if((transfer->peernick == NULL) || wcscmp(transfer->peernick, ires->argv[4].val.str)) + { + if(transfer->peernick != NULL) + free(transfer->peernick); + transfer->peernick = swcsdup(ires->argv[4].val.str); + } + transfer->dir = ires->argv[1].val.num; + transfer->state = ires->argv[2].val.num; + transfer->size = ires->argv[6].val.num; + transfer->curpos = ires->argv[7].val.num; + } else { + transfer = newtransfer(); + transfer->id = ires->argv[0].val.num; + transfer->dir = ires->argv[1].val.num; + transfer->state = ires->argv[2].val.num; + transfer->peerid = swcsdup(ires->argv[3].val.str); + transfer->peernick = swcsdup(ires->argv[4].val.str); + transfer->path = swcsdup(ires->argv[5].val.str); + transfer->size = ires->argv[6].val.num; + transfer->curpos = ires->argv[7].val.num; + transfer->found = 1; + } + dc_freeires(ires); + } + for(transfer = dc_transfers; transfer != NULL; transfer = next) + { + next = transfer->next; + if(!transfer->found) + freetransfer(transfer); + } + data->callback(200, data->data); + free(data); + } else if(resp->code == 201) { + while(dc_transfers != NULL) + freetransfer(dc_transfers); + data->callback(201, data->data); + free(data); + } else if(resp->code == 502) { + while(dc_transfers != NULL) + freetransfer(dc_transfers); + data->callback(502, data->data); + free(data); + } + return(1); +} + +void dc_getfnlistasync(void (*callback)(int, void *), void *udata) +{ + struct gencbdata *data; + + data = smalloc(sizeof(*data)); + data->callback = callback; + data->data = udata; + dc_queuecmd(getfnlistcallback, data, L"lsnodes", NULL); +} + +void dc_gettrlistasync(void (*callback)(int, void *), void *udata) +{ + struct gencbdata *data; + + data = smalloc(sizeof(*data)); + data->callback = callback; + data->data = udata; + dc_queuecmd(gettrlistcallback, data, L"lstrans", NULL); +} + +void dc_uimisc_disconnected(void) +{ + while(dc_fnetnodes != NULL) + freefn(dc_fnetnodes); + while(dc_transfers != NULL) + freetransfer(dc_transfers); +} + +void dc_uimisc_handlenotify(struct dc_response *resp) +{ + struct dc_fnetnode *fn; + struct dc_transfer *transfer; + struct dc_intresp *ires; + + if((ires = dc_interpret(resp)) == NULL) + return; + switch(resp->code) + { + case 601: + if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL) + fn->state = ires->argv[1].val.num; + break; + case 602: + if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL) + { + if(fn->name != NULL) + free(fn->name); + fn->name = swcsdup(ires->argv[1].val.str); + } + break; + case 603: + if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL) + freefn(fn); + break; + case 604: + fn = newfn(); + fn->id = ires->argv[0].val.num; + if(fn->fnet != NULL) + free(fn->fnet); + fn->fnet = swcsdup(ires->argv[1].val.str); + fn->state = DC_FNN_STATE_SYN; + fn->numusers = 0; + break; + case 605: + if((fn = dc_findfnetnode(ires->argv[0].val.num)) != NULL) + fn->numusers = ires->argv[1].val.num; + break; + case 610: + transfer = newtransfer(); + transfer->id = ires->argv[0].val.num; + transfer->dir = ires->argv[1].val.num; + if(transfer->dir == DC_TRNSD_UP) + transfer->state = DC_TRNS_HS; + transfer->peerid = swcsdup(ires->argv[2].val.str); + if(ires->argv[3].val.str[0]) + transfer->path = swcsdup(ires->argv[3].val.str); + break; + case 611: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + transfer->state = ires->argv[1].val.num; + break; + case 612: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + if(transfer->peernick != NULL) + free(transfer->peernick); + transfer->peernick = swcsdup(ires->argv[1].val.str); + } + break; + case 613: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + transfer->size = ires->argv[1].val.num; + break; + case 614: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + transfer->error = ires->argv[1].val.num; + time(&transfer->errortime); + } + break; + case 615: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + transfer->curpos = ires->argv[1].val.num; + break; + case 616: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + { + if(transfer->path != NULL) + free(transfer->path); + transfer->path = swcsdup(ires->argv[1].val.str); + } + break; + case 617: + if((transfer = dc_findtransfer(ires->argv[0].val.num)) != NULL) + freetransfer(transfer); + break; + default: + break; + } + dc_freeires(ires); + resp->curline = 0; +} + +/* Note the backspace handling - it's not as elegant as possible, but + * it helps avoid the "box-of-toothpicks" syndrome when writing search + * expressions manually. */ +wchar_t **dc_lexsexpr(wchar_t *sexpr) +{ + wchar_t **ret; + wchar_t *buf; + size_t retsize, retdata, bufsize, bufdata; + int state; + + ret = NULL; + buf = NULL; + retsize = retdata = bufsize = bufdata = 0; + state = 0; + while(*sexpr != L'\0') + { + switch(state) + { + case 0: + if(!iswspace(*sexpr)) + state = 1; + else + sexpr++; + break; + case 1: + if(iswspace(*sexpr)) + { + if(buf != NULL) + { + addtobuf(buf, L'\0'); + addtobuf(ret, buf); + buf = NULL; + bufsize = bufdata = 0; + } + state = 0; + } else if((*sexpr == L'(') || + (*sexpr == L')') || + (*sexpr == L'&') || + (*sexpr == L'|') || + (*sexpr == L'!')) { + if(buf != NULL) + { + addtobuf(buf, L'\0'); + addtobuf(ret, buf); + buf = NULL; + bufsize = bufdata = 0; + } + addtobuf(buf, *sexpr); + addtobuf(buf, L'\0'); + addtobuf(ret, buf); + buf = NULL; + bufsize = bufdata = 0; + sexpr++; + } else if(*sexpr == L'\"') { + sexpr++; + state = 2; + } else if(*sexpr == L'\\') { + sexpr++; + if(*sexpr == L'\0') + { + addtobuf(buf, *sexpr); + } else if((*sexpr == L'\\') || (*sexpr == L'\"')) { + addtobuf(buf, *sexpr); + sexpr++; + } else { + addtobuf(buf, L'\\'); + addtobuf(buf, *sexpr); + sexpr++; + } + } else { + addtobuf(buf, *(sexpr++)); + } + break; + case 2: + if(*sexpr == L'\\') + { + sexpr++; + if(*sexpr == L'\0') + { + addtobuf(buf, *sexpr); + } else if((*sexpr == L'\\') || (*sexpr == L'\"')) { + addtobuf(buf, *sexpr); + sexpr++; + } else { + addtobuf(buf, L'\\'); + addtobuf(buf, *sexpr); + sexpr++; + } + } else if(*sexpr == L'\"') { + state = 1; + sexpr++; + } else { + addtobuf(buf, *(sexpr++)); + } + break; + } + } + if(buf != NULL) + { + addtobuf(buf, L'\0'); + addtobuf(ret, buf); + } + addtobuf(ret, NULL); + return(ret); +} + +void dc_freewcsarr(wchar_t **arr) +{ + wchar_t **buf; + + if(arr == NULL) + return; + for(buf = arr; *buf != NULL; buf++) + free(*buf); + free(arr); +} diff --git a/lib/utils.c b/lib/utils.c new file mode 100644 index 0000000..f5cd7d0 --- /dev/null +++ b/lib/utils.c @@ -0,0 +1,239 @@ +/* + * Dolda Connect - Modular multiuser Direct Connect-style client + * Copyright (C) 2004 Fredrik Tolf (fredrik@dolda2000.com) + * + * 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 +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include +#endif +#include + +extern int vswprintf (wchar_t *__restrict __s, size_t __n, + __const wchar_t *__restrict __format, + __gnuc_va_list __arg); + +char *vsprintf2(char *format, va_list al) +{ + int ret; + char *buf; + + ret = vsnprintf(NULL, 0, format, al); + if((buf = malloc(ret + 1)) == NULL) + { + return(NULL); + } + vsnprintf(buf, ret + 1, format, al); + return(buf); +} + +char *sprintf2(char *format, ...) +{ + va_list args; + char *buf; + + va_start(args, format); + buf = vsprintf2(format, args); + va_end(args); + return(buf); +} + +wchar_t *vswprintf2(wchar_t *format, va_list al) +{ + int ret; + wchar_t *buf; + size_t bufsize; + + buf = smalloc(sizeof(wchar_t) * (bufsize = 1024)); + while((ret = vswprintf(buf, bufsize, format, al)) < 0) + buf = srealloc(buf, sizeof(wchar_t) * (bufsize *= 2)); + if(bufsize > ret + 1) + buf = srealloc(buf, sizeof(wchar_t) * (ret + 1)); + return(buf); +} + +wchar_t *swprintf2(wchar_t *format, ...) +{ + va_list args; + wchar_t *buf; + + va_start(args, format); + buf = vswprintf2(format, args); + va_end(args); + return(buf); +} + +wchar_t *icmbstowcs(char *mbs, char *charset) +{ + int ret; + char *buf; + char *p, *p2; + size_t len1, len2, bufsize, data; + iconv_t cd; + + len1 = strlen(mbs) + 1; + bufsize = len2 = len1 * sizeof(wchar_t); + if((buf = malloc(bufsize)) == NULL) + { + return(NULL); + } + if(charset == NULL) + charset = nl_langinfo(CODESET); + if((cd = iconv_open("wchar_t", charset)) == (iconv_t)-1) + { + free(buf); + return(NULL); + } + p = buf; + while(len1 > 0) + { + ret = iconv(cd, &mbs, &len1, &p, &len2); + if(ret < 0) + { + if(errno == E2BIG) + { + data = p - buf; + len2 += 128; + bufsize += 128; + if((p2 = realloc(buf, bufsize)) == NULL) + { + free(buf); + return(NULL); + } + buf = p2; + p = buf + data; + } else { + free(buf); + return(NULL); + } + } + } + if(len2 > 0) + buf = realloc(buf, p - buf); + iconv_close(cd); + return((wchar_t *)buf); +} + +wchar_t *icsmbstowcs(char *mbs, char *charset, wchar_t *def) +{ + static wchar_t *buf = NULL; + + if(buf != NULL) + free(buf); + if((buf = icmbstowcs(mbs, charset)) == NULL) + return(def); + return(buf); +} + +char *icwcstombs(wchar_t *wcs, char *charset) +{ + int ret; + char *buf; + char *p, *p2; + size_t len1, len2, bufsize, data; + iconv_t cd; + + len1 = sizeof(wchar_t) * (wcslen(wcs) + 1); + bufsize = len2 = len1; + if((buf = malloc(bufsize)) == NULL) + { + return(NULL); + } + if(charset == NULL) + charset = nl_langinfo(CODESET); + if((cd = iconv_open(charset, "wchar_t")) == (iconv_t)-1) + { + free(buf); + return(NULL); + } + p = buf; + while(len1 > 0) + { + ret = iconv(cd, (char **)&wcs, &len1, &p, &len2); + if(ret < 0) + { + if(errno == E2BIG) + { + data = p - buf; + len2 += 128; + bufsize += 128; + if((p2 = realloc(buf, bufsize)) == NULL) + { + free(buf); + return(NULL); + } + buf = p2; + p = buf + data; + } else { + free(buf); + return(NULL); + } + } + } + if(len2 > 0) + buf = realloc(buf, p - buf); + iconv_close(cd); + return(buf); +} + +char *icswcstombs(wchar_t *wcs, char *charset, char *def) +{ + static char *buf = NULL; + + if(buf != NULL) + free(buf); + if((buf = icwcstombs(wcs, charset)) == NULL) + return(def); + return(buf); +} + +wchar_t *wcstolower(wchar_t *wcs) +{ + wchar_t *p; + + for(p = wcs; *p != L'\0'; p++) + *p = towlower(*p); + return(wcs); +} + +void _sizebuf(void **buf, size_t *bufsize, size_t reqsize, size_t elsize, int algo) +{ + if(*bufsize >= reqsize) + return; + switch(algo) + { + case 0: + *buf = srealloc(*buf, elsize * ((*bufsize) = reqsize)); + break; + case 1: + if(*bufsize == 0) + *bufsize = 1; + while(*bufsize < reqsize) + *bufsize *= 2; + *buf = srealloc(*buf, elsize * (*bufsize)); + break; + } +} diff --git a/po/CVS/Entries b/po/CVS/Entries new file mode 100644 index 0000000..0d64cf3 --- /dev/null +++ b/po/CVS/Entries @@ -0,0 +1,6 @@ +/ChangeLog/1.1/Fri Aug 13 18:08:40 2004// +/LINGUAS/1.2/Fri Aug 13 23:00:24 2004// +/Makevars/1.1/Fri Aug 13 18:08:41 2004// +/POTFILES.in/1.2/Sat Jan 1 17:37:01 2005// +/sv.po/1.12/Fri Oct 14 23:22:19 2005// +D diff --git a/po/CVS/Repository b/po/CVS/Repository new file mode 100644 index 0000000..4a5c94d --- /dev/null +++ b/po/CVS/Repository @@ -0,0 +1 @@ +doldaconnect/po diff --git a/po/CVS/Root b/po/CVS/Root new file mode 100644 index 0000000..2886064 --- /dev/null +++ b/po/CVS/Root @@ -0,0 +1 @@ +:ext:dolda2000@cvs.sourceforge.net:/cvsroot/doldaconnect diff --git a/po/ChangeLog b/po/ChangeLog new file mode 100644 index 0000000..4e6a12b --- /dev/null +++ b/po/ChangeLog @@ -0,0 +1,11 @@ +2004-08-13 gettextize + + * Makefile.in.in: New file, from gettext-0.12.1. + * boldquot.sed: New file, from gettext-0.12.1. + * en@boldquot.header: New file, from gettext-0.12.1. + * en@quot.header: New file, from gettext-0.12.1. + * insert-header.sin: New file, from gettext-0.12.1. + * quot.sed: New file, from gettext-0.12.1. + * remove-potcdate.sin: New file, from gettext-0.12.1. + * Rules-quot: New file, from gettext-0.12.1. + diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..b917a40 --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1,2 @@ +sv + diff --git a/po/Makevars b/po/Makevars new file mode 100644 index 0000000..eb62ce1 --- /dev/null +++ b/po/Makevars @@ -0,0 +1,41 @@ +# Makefile variables for PO directory in any package using GNU gettext. + +# Usually the message domain is the same as the package name. +DOMAIN = $(PACKAGE) + +# These two variables depend on the location of this directory. +subdir = po +top_builddir = .. + +# These options get passed to xgettext. +XGETTEXT_OPTIONS = --keyword=_ --keyword=N_ + +# This is the copyright holder that gets inserted into the header of the +# $(DOMAIN).pot file. Set this to the copyright holder of the surrounding +# package. (Note that the msgstr strings, extracted from the package's +# sources, belong to the copyright holder of the package.) Translators are +# expected to transfer the copyright for their translations to this person +# or entity, or to disclaim their copyright. The empty string stands for +# the public domain; in this case the translators are expected to disclaim +# their copyright. +COPYRIGHT_HOLDER = Fredrik Tolf + +# This is the email address or URL to which the translators shall report +# bugs in the untranslated strings: +# - Strings which are not entire sentences, see the maintainer guidelines +# in the GNU gettext documentation, section 'Preparing Strings'. +# - Strings which use unclear terms or require additional context to be +# understood. +# - Strings which make invalid assumptions about notation of date, time or +# money. +# - Pluralisation problems. +# - Incorrect English spelling. +# - Incorrect formatting. +# It can be your email address, or a mailing list address where translators +# can write to without being subscribed, or the URL of a web page through +# which the translators can contact you. +MSGID_BUGS_ADDRESS = fredrik@dolda2000.com + +# This is the list of locale categories, beyond LC_MESSAGES, for which the +# message catalogs shall be used. It is usually empty. +EXTRA_LOCALE_CATEGORIES = diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..1b57489 --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,7 @@ +clients/gtk2/main.c +clients/gtk2/inpdialog.gtk +clients/gtk2/mainwnd.gtk +clients/gtk2/pref.gtk +clients/gnome-trans-applet/dolcon-trans-applet.c +clients/gnome-trans-applet/conduit.c +clients/gnome-trans-applet/conduit-pipe.c diff --git a/po/sv.po b/po/sv.po new file mode 100644 index 0000000..f9d6287 --- /dev/null +++ b/po/sv.po @@ -0,0 +1,439 @@ +# Swedish translations for doldaconnect package +# Svenska översättningar för paket doldaconnect. +# Copyright (C) 2004 Fredrik Tolf +# This file is distributed under the same license as the doldaconnect package. +# Fredrik Tolf , 2004. +# +msgid "" +msgstr "" +"Project-Id-Version: doldaconnect 0.1.1\n" +"Report-Msgid-Bugs-To: fredrik@dolda2000.com\n" +"POT-Creation-Date: 2005-07-09 05:31+0200\n" +"PO-Revision-Date: 2005-08-15 03:48+0200\n" +"Last-Translator: Fredrik Tolf \n" +"Language-Team: Swedish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: clients/gtk2/main.c:215 clients/gtk2/main.c:238 clients/gtk2/main.c:270 +#: clients/gtk2/main.c:304 clients/gtk2/main.c:400 clients/gtk2/main.c:419 +msgid "Unknown" +msgstr "Okänt" + +#: clients/gtk2/main.c:319 +msgid "Not found" +msgstr "Kunde inte hittas" + +#: clients/gtk2/main.c:321 +msgid "No slots" +msgstr "Inga slots lediga" + +#: clients/gtk2/main.c:322 +msgid "%H:%M:%S" +msgstr "%H:%M:%S" + +#: clients/gtk2/main.c:323 +#, c-format +msgid "%s (reported at %s)" +msgstr "%s (rapporterades kl. %s)" + +#: clients/gtk2/main.c:484 clients/gtk2/main.c:1807 +msgid "Disconnected" +msgstr "Frånkopplad" + +#: clients/gtk2/main.c:607 +msgid "Could not get your home directory!" +msgstr "Kunde inte hitta din hemkatalog!" + +#: clients/gtk2/main.c:614 +#, c-format +msgid "Could not open configuration file for writing: %s" +msgstr "Kunde inte skriva till konfigurationsfilen: %s" + +#: clients/gtk2/main.c:655 clients/gtk2/main.c:659 +msgid "Login" +msgstr "Logga in" + +#: clients/gtk2/main.c:695 +msgid "Could not negotiate an acceptable authentication mechanism" +msgstr "Kunde inte förhandla fram en acceptabel autentiseringsmekanism" + +#: clients/gtk2/main.c:700 +msgid "The server has encountered an error" +msgstr "Servern har råkat ut för ett fel" + +#: clients/gtk2/main.c:705 +msgid "Internal client error" +msgstr "Internt fel i klienten" + +#: clients/gtk2/main.c:714 +msgid "Login attempt failed!" +msgstr "Inloggningsförsöket misslyckades!" + +#: clients/gtk2/main.c:760 +msgid "Discrete sizes" +msgstr "Enskilda storlekar" + +#: clients/gtk2/main.c:881 +msgid "Connected" +msgstr "Ansluten" + +#: clients/gtk2/main.c:884 +msgid "The server refused the connection" +msgstr "Servern vägrade förbindelsen" + +#: clients/gtk2/main.c:1083 +msgid "The server has closed the connection" +msgstr "Servern har stängt förbindelsen" + +#: clients/gtk2/main.c:1085 +#, c-format +msgid "" +"The connection to the server failed:\n" +"\n" +"%s" +msgstr "" +"Anslutning till servern misslyckades:\n" +"\n" +"%s" + +#: clients/gtk2/main.c:1105 +msgid "Preferences" +msgstr "Inställningar" + +#: clients/gtk2/main.c:1131 +#, c-format +msgid "" +"Could not connect:\n" +"\n" +"%s" +msgstr "" +"Kunde inte ansluta:\n" +"\n" +"%s" + +#: clients/gtk2/main.c:1138 +#: clients/gnome-trans-applet/dolcon-trans-applet.c:84 +msgid "Connecting..." +msgstr "Ansluter..." + +#: clients/gtk2/main.c:1147 +msgid "Connect" +msgstr "Anslut" + +#: clients/gtk2/main.c:1147 +msgid "Server address:" +msgstr "Serverns adress:" + +#: clients/gtk2/main.c:1160 clients/gtk2/main.c:1181 clients/gtk2/main.c:1360 +#: clients/gtk2/main.c:1402 clients/gtk2/main.c:1464 clients/gtk2/main.c:1552 +#: clients/gtk2/main.c:1647 +msgid "Not connected to DC server" +msgstr "Ej ansluten till DC-servern" + +#: clients/gtk2/main.c:1167 clients/gtk2/main.c:1195 clients/gtk2/main.c:1373 +#: clients/gtk2/main.c:1420 clients/gtk2/main.c:1477 clients/gtk2/main.c:1570 +#: clients/gtk2/main.c:1625 clients/gtk2/main.c:1687 +msgid "You do not have permission to do that" +msgstr "Du har inte tillstånd att göra det" + +#: clients/gtk2/main.c:1197 clients/gtk2/main.c:1422 +msgid "The server could not parse that address" +msgstr "Servern kunde inte tolka den adressen" + +#: clients/gtk2/main.c:1225 +#, c-format +msgid "Could not read from public hub listing process: %s" +msgstr "Kunde inte läsa från processen som hämtar den publika hublistan: %s" + +#: clients/gtk2/main.c:1262 +#, c-format +msgid "Could not decode hublist - aborting at this point: %s" +msgstr "Kunde inte dekoda hublistan - stannar här: %s" + +#: clients/gtk2/main.c:1365 clients/gtk2/main.c:1469 +msgid "No hub selected" +msgstr "Ingen hub vald" + +#: clients/gtk2/main.c:1479 +msgid "" +"This hub could not support all the types of characters in your chat message" +msgstr "Den här hubben klarar inte av alla sorters bokstäver i ditt meddelande" + +#: clients/gtk2/main.c:1481 +msgid "This hub does not support chatting" +msgstr "Den här hubben klarar inte av att chatta" + +#: clients/gtk2/main.c:1483 +#, c-format +msgid "An error occurred while trying to chat (%i)" +msgstr "Ett fel uppstod under försöket att chatta (%i)" + +#: clients/gtk2/main.c:1560 +msgid "Please enter a search expression before searching" +msgstr "Skriv in ett sökuttryck först" + +#: clients/gtk2/main.c:1568 +msgid "Could not find any hubs to search on" +msgstr "Kunde inte hitta någon hub att söka på" + +#: clients/gtk2/main.c:1572 +msgid "The server could not parse your search expression" +msgstr "Servern kunde inte tolka ditt sökuttryck" + +#: clients/gtk2/main.c:1574 +#, c-format +msgid "An error occurred while trying to search (%i)" +msgstr "Ett fel uppstod under försöket att söka (%i)" + +#: clients/gtk2/main.c:1627 +#, c-format +msgid "An error occurred while trying to cancel (%i)" +msgstr "Ett fel uppstod under försöket att avbryta (%i)" + +#: clients/gtk2/main.c:1689 +#, c-format +msgid "An error occurred while trying to queue the download (%i)" +msgstr "Ett fel uppstod under försöket att lägga till nerladdningen (%i)" + +#: clients/gtk2/main.c:1720 clients/gtk2/mainwnd.gtk:398 +#, c-format +msgid "Ready to search" +msgstr "Redo att söka" + +#: clients/gtk2/main.c:1722 +#, c-format +msgid "Search scheduled and will be submitted in %i seconds" +msgstr "Sökningen är schemalagd och kommer genomföras om %i sekunder" + +#: clients/gtk2/inpdialog.gtk:16 +msgid " " +msgstr " " + +#: clients/gtk2/mainwnd.gtk:45 +msgid "_Main" +msgstr "Huvud_meny" + +#: clients/gtk2/mainwnd.gtk:50 +msgid "_Connect" +msgstr "_Anslut" + +#: clients/gtk2/mainwnd.gtk:57 +msgid "_Disconnect" +msgstr "_Koppla från" + +#: clients/gtk2/mainwnd.gtk:69 +msgid "_Shut down daemon" +msgstr "_Stäng av demonen" + +#: clients/gtk2/mainwnd.gtk:84 +msgid "Op_tions" +msgstr "Alternati_v" + +#: clients/gtk2/mainwnd.gtk:112 +msgid "Connected hu_bs" +msgstr "_Anslutna hubbar" + +#: clients/gtk2/mainwnd.gtk:124 clients/gtk2/mainwnd.gtk:260 +msgid "Hub name" +msgstr "Hubnamn" + +#: clients/gtk2/mainwnd.gtk:138 clients/gtk2/mainwnd.gtk:175 +msgid "# users" +msgstr "Antal användare" + +#: clients/gtk2/mainwnd.gtk:149 +msgid "D_isconnect" +msgstr "_Koppla från" + +#: clients/gtk2/mainwnd.gtk:160 +msgid "_Public hub list" +msgstr "_Publik hublista" + +#: clients/gtk2/mainwnd.gtk:180 +msgid "Name" +msgstr "Namn" + +#: clients/gtk2/mainwnd.gtk:185 +msgid "Description" +msgstr "Beskrivning" + +#: clients/gtk2/mainwnd.gtk:198 +msgid "_Filter:" +msgstr "_Filter:" + +#: clients/gtk2/mainwnd.gtk:209 +msgid "_Get public hub list" +msgstr "Hämta _publik hublista" + +#: clients/gtk2/mainwnd.gtk:214 +msgid "_Address:" +msgstr "_Adress:" + +#: clients/gtk2/mainwnd.gtk:225 +msgid "C_onnect" +msgstr "_Anslut" + +#: clients/gtk2/mainwnd.gtk:239 +msgid "_Hub connections" +msgstr "_Hubanslutningar" + +#: clients/gtk2/mainwnd.gtk:247 +msgid "Hu_bs" +msgstr "Hu_bbar" + +#: clients/gtk2/mainwnd.gtk:298 +msgid "Chat st_ring:" +msgstr "Chatm_eddelande:" + +#: clients/gtk2/mainwnd.gtk:309 +msgid "S_end" +msgstr "S_kicka" + +#: clients/gtk2/mainwnd.gtk:321 +msgid "_Chat" +msgstr "_Chat" + +#: clients/gtk2/mainwnd.gtk:328 +msgid "S_imple search:" +msgstr "_Enkel sökning:" + +#: clients/gtk2/mainwnd.gtk:340 +msgid "S_earch" +msgstr "S_ök" + +#: clients/gtk2/mainwnd.gtk:346 +msgid "C_ancel" +msgstr "_Avbryt" + +#: clients/gtk2/mainwnd.gtk:357 +msgid "Displa_y results with free slots only" +msgstr "_Visa endast resultat med fria slots" + +#: clients/gtk2/mainwnd.gtk:363 +msgid "Ad_vanced" +msgstr "A_vancerat" + +#: clients/gtk2/mainwnd.gtk:368 +msgid "C_omplete search expression:" +msgstr "_Komplett sökuttryck:" + +#: clients/gtk2/mainwnd.gtk:380 +msgid "Filter ar_gument:" +msgstr "Filterar_gument:" + +#: clients/gtk2/mainwnd.gtk:407 +msgid "Search _results:" +msgstr "Sök_resultat:" + +#: clients/gtk2/mainwnd.gtk:422 +msgid "#" +msgstr "#" + +#: clients/gtk2/mainwnd.gtk:432 +msgid "Peer name" +msgstr "Användare" + +#: clients/gtk2/mainwnd.gtk:437 +msgid "File name" +msgstr "Filnamn" + +#: clients/gtk2/mainwnd.gtk:444 clients/gtk2/mainwnd.gtk:520 +#: clients/gtk2/mainwnd.gtk:590 +msgid "Size" +msgstr "Storlek" + +#: clients/gtk2/mainwnd.gtk:454 +msgid "Slots" +msgstr "Slots" + +#: clients/gtk2/mainwnd.gtk:460 +msgid "Known speed" +msgstr "Känd hastighet" + +#: clients/gtk2/mainwnd.gtk:470 +msgid "Rsp. time" +msgstr "Svarstid" + +#: clients/gtk2/mainwnd.gtk:482 +msgid "_Search" +msgstr "_Sök" + +#: clients/gtk2/mainwnd.gtk:492 +msgid "_List of downloads:" +msgstr "_Lista över nerladdningar:" + +#: clients/gtk2/mainwnd.gtk:504 clients/gtk2/mainwnd.gtk:574 +msgid "User Name" +msgstr "Användarnamn" + +#: clients/gtk2/mainwnd.gtk:516 clients/gtk2/mainwnd.gtk:586 +msgid "File Name" +msgstr "Filenamn" + +#: clients/gtk2/mainwnd.gtk:529 clients/gtk2/mainwnd.gtk:599 +msgid "Position" +msgstr "Position" + +#: clients/gtk2/mainwnd.gtk:542 +msgid "Error" +msgstr "Felmeddelande" + +#: clients/gtk2/mainwnd.gtk:557 +msgid "_Downloads" +msgstr "_Nerladdningar" + +#: clients/gtk2/mainwnd.gtk:562 +msgid "_List of uploads:" +msgstr "_Lista över uppladdningar:" + +#: clients/gtk2/mainwnd.gtk:618 +msgid "_Uploads" +msgstr "_Uppladdningar" + +#: clients/gtk2/pref.gtk:12 +msgid "_Public hub list URL:" +msgstr "_URL för publik hublista:" + +#: clients/gtk2/pref.gtk:22 +msgid "_Dolda connect user name:" +msgstr "Användarnamn för _Dolda Connect:" + +#: clients/gtk2/pref.gtk:32 +msgid "Dolda Connect _server:" +msgstr "Dolda Connect-_server:" + +#: clients/gtk2/pref.gtk:42 +msgid "Connect _automatically on startup" +msgstr "Anslut au_tomatiskt vid uppstart" + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:57 +msgid "Calculating remaining time..." +msgstr "Beräknar återstående tid..." + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:62 +msgid "Time left: Infinite (Transfer is standing still)" +msgstr "Tid kvar: Oändlig (Överföringen står still)" + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:66 +#, c-format +msgid "Time left: %i:%02i" +msgstr "Tid kvar: %i:%02i" + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:80 +msgid "Not connected" +msgstr "Ej ansluten" + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:92 +msgid "No transfers to display" +msgstr "Det finns inga överföringar att visa" + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:96 +msgid "No transfer selected" +msgstr "Ingen överföring vald" + +#: clients/gnome-trans-applet/dolcon-trans-applet.c:105 +msgid "Initializing" +msgstr "Förbereder" -- 2.11.0