diff --git a/.boring b/.boring new file mode 100644 index 0000000..25deac6 --- /dev/null +++ b/.boring @@ -0,0 +1,14 @@ +# Boring file regexps: +(^|/)_darcs($|/) +(^|/)CVS($|/) +(^|/)\.svn($|/) +(^|/)\.DS_Store$ +(^|/)Thumbs\.db$ +\# +~$ +(^|/)core(\.[0-9]+)?$ +\.(pyc|pyo|o|so|orig|bak|BAK|prof|wpu|cvsignore)$ +(^|/)build($|/) +(^|/)dist($|/) +(^|/)\.komodotools($|/) +^MANIFEST$ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..88bf7a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,680 @@ +# Copyright 2010-2011 AG Projects +# http://ag-projects.com + +License + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. 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 +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. 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. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + 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 +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program 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, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU 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 Lesser General +Public License instead of this License. But first, please read +. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c9bfea3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +recursive-include debian changelog compat control copyright rules +recursive-include debian pycompat pyversions +recursive-include debian *.init *.dirs *.default +recursive-include debian/source format +recursive-include resources/sounds *.wav +recursive-include resources/sounds/moh *.wav +prune debian/tmp +prune debian/sylkserver-* +include INSTALL LICENSE MANIFEST.in *.ini.sample diff --git a/README b/README new file mode 100644 index 0000000..af7b878 --- /dev/null +++ b/README @@ -0,0 +1,103 @@ + +SylkServer +---------- + +Copyright (c) 2010-2011 AG Projects +http://ag-projects.com + +Authors: Adrian Georgescu, Denis Bilenko, Saul Ibarra +Home page: http://sylkserver.com + + +License +------- + +SylkServer is licensed under GNU General Public License version 3. A copy of +the license is available at http://www.fsf.org/licensing/licenses/agpl-3.0.html + + +Description +----------- + +SylkServer is a state of the art, extensible SIP Application Server + +SylkServer allows creation and delivery of rich multimedia applications +accessed by SIP User Agents. The server supports SIP signaling over TLS, +TCP and UDP transports, RTP and MSRP media planes, has built in capabilities +for creating ad-hoc SIP multimedia conferences with HD Audio, IM and File +Transfer and can be easily extended with other applications by using Python +programming language. + + +Features +-------- + +SIP Signaling + + - TLS, TCP and UDP transports + - INVITE and MESSAGE for sessions + - SUBSCRIBE/NOTIFY for conference event notifications + +Audio + + - G722 and Speex wideband codecs + - G711 and GSM narrow-band codecs + - sRTP encryption + - RTP timeout + +Instant Messaging + + - MSRP chat and SIP MESSAGE + - CPIM envelope + - Is-composing indicator + - History buffer + +Conferencing + + - RTP mixer + - MSRP Switch, private messgaing + - Conference event package + - isfocus + + +Built-in Applications +--------------------- + +1. Conferencing + +SylkServer allows SIP end-points to create ad-hoc conference rooms by +sending INVITE to a random username at the hostname or domain where the +server runs. Other participants can then join by sending an INVITE to the +same SIP URI used to create the room. The INVITE and subsequent re-INVITE +methods may contain one or more media types supported by the server. Each +conference room mixed audio, instant messages and uploded files are +dispatched to all participants. + +SIP end-points that do not support MSRP chat can join the bridge by using +audio only, they will receive the chat messages over the SIP signaling using +SIP MESSAGE method, which is supported by many legacy end-points. Messages +sent to the room using SIP MESSAGE will be dispatched by either SIP MESSAGE +or through established MSRP sessions depending on how the end-points have +joined the room. + +If a participant sends a file to the SIP URI of the room, the server will +accept it, store it for the duration of the conference and offer it to all +participants either present at that moment, or later to those that have +joined the conference at a later moment. + + +Standards +--------- + +The server implements relevant features from the following standards: + + * MSRP and its relay extension RFC4975, RFC4976 + * File Transfer over RFC5547 + * Indication of Message Composition RFC3994 + * CPIM Message Format RFC3862 + * Conference event package RFC4575 + * A Framework for Conferencing with SIP RFC4353 + * Conferencing for User Agents RFC4579 + 5.1. INVITE: Joining a Conference Using the Conference URI - Dial-In + * MSRP switch draft-ietf-simple-chat-07 + diff --git a/conference.ini.sample b/conference.ini.sample new file mode 100644 index 0000000..f01ab88 --- /dev/null +++ b/conference.ini.sample @@ -0,0 +1,18 @@ +; SylkServer Conference application configuration file + +[Conference] + +; The following settings are the default used by the software, uncomment them +; only if you want to make changes + +; db_uri = sqlite:///var/lib/sylkserver/conference.sqlite + +; Table for storing messages history +; history_table = message_history + +; Playback the last messages after join a room +; replay_history = 20 + +; Use MESSAGE for participants that joined a conference room with audio but without MSRP chat +; enable_sip_message = False + diff --git a/config.ini.sample b/config.ini.sample new file mode 100644 index 0000000..6282a67 --- /dev/null +++ b/config.ini.sample @@ -0,0 +1,64 @@ +; SylkServer configuration file + +[Server] + +; The following settings are the default used by the software, uncomment +; them only if you want to make changes + +; default_application = conference + +; trace_dir = /var/log/sylkserver +; trace_sip = False +; trace_msrp = False +; trace_notifications = False + +; TLS can be used for encryption of SIP signaling and MSRP media. TLS is +; disabled by default. To enabled TLS, you must have a valid X.509 +; certificate and configure it below, then set the local_tls_port in the SIP +; section and use_tls in MSRP section + +; The X.509 Certificate Authorities file +; ca_file = /etc/sylkserver/tls/ca.crt + +; The file containing X.509 certificate and private key in unencrypted format +; certificate = /etc/sylkserver/tls/sylkserver.crt + +; verify_server = False + + +[SIP] + +; SIP transport settings +; IP address used for SIP signaling; empty string or any means listen on interface used +; by the default route +; local_ip = + +; Ports used for SIP transports, if not set to any value the transport will be disabled +; local_udp_port = 5060 +; local_tcp_port = 5060 +; local_tls_port = + + +[MSRP] + +; MSRP transport settings + +; use_tls = False + + +[RTP] + +; RTP transport settings + +; Allowed codec list, valid values: G722, speex, PCMU, PCMA, iLBC, GSM +; allowed_codecs = G722,speex,PCMU,PCMA + +; Port range used for RTP +; port_range = 50000:50500 + +; SRTP valid values: disabled, mandatory, optional +; srtp_encryption = optional + +; RTP stream timeout, session will be disconnected after this value +; timeout = 30 + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..11fe185 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +sylkserver (1.0.0) unstable; urgency=low + + * Initial release + + -- Saul Ibarra Thu, 27 Jan 2011 17:43:11 +0100 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..f385732 --- /dev/null +++ b/debian/control @@ -0,0 +1,19 @@ +Source: sylkserver +Section: net +Priority: optional +Maintainer: Saul Ibarra +Uploaders: Dan Pascu , Adrian Georgescu +Build-Depends: cdbs (>= 0.4.53), debhelper (>= 7), python-all (>= 2.6), python-support +Standards-Version: 3.9.1 + +Package: sylkserver +Architecture: all +Depends: ${python:Depends}, ${misc:Depends}, python-application (>= 1.2.4), python-sipsimple (>= 0.17.0), python-sqlobject (>= 0.12.4) +Suggests: python-mysqldb +Description: A state of the art, extensible SIP Application Server + SylkServer allows creation and delivery of rich multimedia applications + accessed by SIP User Agents. The server supports SIP signaling over TLS, + TCP and UDP transports, RTP and MSRP media planes, has built in + capabilities for creating ad-hoc SIP multimedia conferences with HD Audio, + IM and File Transfer and can be easily extended with other applications by + using Python programming language. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..c9f3361 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,23 @@ +This work was packaged for Debian by: + + Saul Ibarra on Tue Jan 25 17:21:12 CET 2011 + +Copyright: + + Copyright (C) 2010 AG Projects + +License: + + SylkServer is licensed under GNU General Public License version 3. A copy of the + license is available at /usr/share/common-licenses/GPL-3 or online at: + http://www.gnu.org/licenses/gpl-3.0.html + + The following restrictions apply: + * You may not alter the name of the software (SylkServer) + * You may not alter the Copyright and About notices + +The Debian packaging is: + + Copyright (C) 2010 Saul Ibarra + +and is licensed under the GPL version 3. diff --git a/debian/pycompat b/debian/pycompat new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/debian/pycompat @@ -0,0 +1 @@ +2 diff --git a/debian/pyversions b/debian/pyversions new file mode 100644 index 0000000..0c043f1 --- /dev/null +++ b/debian/pyversions @@ -0,0 +1 @@ +2.6- diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..e4a399b --- /dev/null +++ b/debian/rules @@ -0,0 +1,15 @@ +#!/usr/bin/make -f + +DEB_PYTHON_SYSTEM=pysupport +DEB_DH_INSTALLINIT_ARGS=--no-start + +include /usr/share/cdbs/1/class/python-distutils.mk +include /usr/share/cdbs/1/rules/debhelper.mk + +clean:: + -rm -rf build dist MANIFEST + +install/sylkserver:: + install -m 0644 config.ini.sample debian/sylkserver/etc/sylkserver/config.ini + install -m 0644 conference.ini.sample debian/sylkserver/etc/sylkserver/conference.ini + diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/sylkserver.default b/debian/sylkserver.default new file mode 100644 index 0000000..c489035 --- /dev/null +++ b/debian/sylkserver.default @@ -0,0 +1,8 @@ +# +# SylkServer startup options +# + +# Set to yes to enable SylkServer, once configured properly +# by editing /etc/sylkserver/config.ini +RUN_SYLKSERVER=no + diff --git a/debian/sylkserver.dirs b/debian/sylkserver.dirs new file mode 100644 index 0000000..1c539de --- /dev/null +++ b/debian/sylkserver.dirs @@ -0,0 +1,5 @@ +etc/sylkserver +etc/sylkserver/tls +usr/bin +usr/share/sylkserver +var/lib/sylkserver diff --git a/debian/sylkserver.init b/debian/sylkserver.init new file mode 100644 index 0000000..5fff4b1 --- /dev/null +++ b/debian/sylkserver.init @@ -0,0 +1,72 @@ +#!/bin/sh +# +### BEGIN INIT INFO +# Provides: sylkserver +# Required-Start: $syslog $network $local_fs $remote_fs $time +# Required-Stop: $syslog $network $local_fs $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start the SylkServer +# Description: Start the SylkServer +### END INIT INFO + +PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin + +INSTALL_DIR="/usr/bin" +RUNTIME_DIR="/var/run/sylkserver" +DEFAULTS="/etc/default/sylkserver" +SERVER="$INSTALL_DIR/sylk-server" +PID="$RUNTIME_DIR/server.pid" +OPTIONS="" + +NAME="sylk-server" +DESC="SylkServer" + +test -f $SERVER || exit 0 + +. /lib/lsb/init-functions + +# Load startup options if available +if [ -f $DEFAULTS ]; then + . $DEFAULTS || true +fi + +if [ "$RUN_SYLKSERVER" != "yes" ]; then + echo "SylkServer not yet configured. Edit /etc/default/sylkserver first." + exit 0 +fi + +start() { + log_daemon_msg "Starting $DESC: $NAME " + start-stop-daemon --start --quiet --pidfile $PID \ + --exec $SERVER -- $OPTIONS || log_progress_msg "already running" +} + +stop () { + log_daemon_msg "Stopping $DESC: $NAME " + start-stop-daemon --stop --quiet --oknodo --pidfile $PID +} + +case "$1" in + start) + start + log_end_msg 0 + ;; + stop) + stop + log_end_msg 0 + ;; + restart|force-reload) + stop + sleep 1 + start + log_end_msg 0 + ;; + *) + echo "Usage: /etc/init.d/$NAME {start|stop|restart|force-reload}" >&2 + exit 1 + ;; +esac + +exit 0 + diff --git a/resources/sounds/bi_0.wav b/resources/sounds/bi_0.wav new file mode 100644 index 0000000..d1dbab3 Binary files /dev/null and b/resources/sounds/bi_0.wav differ diff --git a/resources/sounds/bi_1.wav b/resources/sounds/bi_1.wav new file mode 100644 index 0000000..b93fcf5 Binary files /dev/null and b/resources/sounds/bi_1.wav differ diff --git a/resources/sounds/bi_10.wav b/resources/sounds/bi_10.wav new file mode 100644 index 0000000..e47eeaa Binary files /dev/null and b/resources/sounds/bi_10.wav differ diff --git a/resources/sounds/bi_100.wav b/resources/sounds/bi_100.wav new file mode 100644 index 0000000..4eba03d Binary files /dev/null and b/resources/sounds/bi_100.wav differ diff --git a/resources/sounds/bi_1000.wav b/resources/sounds/bi_1000.wav new file mode 100644 index 0000000..4b770cd Binary files /dev/null and b/resources/sounds/bi_1000.wav differ diff --git a/resources/sounds/bi_10th.wav b/resources/sounds/bi_10th.wav new file mode 100644 index 0000000..221c685 Binary files /dev/null and b/resources/sounds/bi_10th.wav differ diff --git a/resources/sounds/bi_11.wav b/resources/sounds/bi_11.wav new file mode 100644 index 0000000..56779d6 Binary files /dev/null and b/resources/sounds/bi_11.wav differ diff --git a/resources/sounds/bi_11th.wav b/resources/sounds/bi_11th.wav new file mode 100644 index 0000000..7fa5230 Binary files /dev/null and b/resources/sounds/bi_11th.wav differ diff --git a/resources/sounds/bi_12.wav b/resources/sounds/bi_12.wav new file mode 100644 index 0000000..8bc458b Binary files /dev/null and b/resources/sounds/bi_12.wav differ diff --git a/resources/sounds/bi_12th.wav b/resources/sounds/bi_12th.wav new file mode 100644 index 0000000..e540b5a Binary files /dev/null and b/resources/sounds/bi_12th.wav differ diff --git a/resources/sounds/bi_13.wav b/resources/sounds/bi_13.wav new file mode 100644 index 0000000..554d168 Binary files /dev/null and b/resources/sounds/bi_13.wav differ diff --git a/resources/sounds/bi_13th.wav b/resources/sounds/bi_13th.wav new file mode 100644 index 0000000..84d7718 Binary files /dev/null and b/resources/sounds/bi_13th.wav differ diff --git a/resources/sounds/bi_14.wav b/resources/sounds/bi_14.wav new file mode 100644 index 0000000..fc661ac Binary files /dev/null and b/resources/sounds/bi_14.wav differ diff --git a/resources/sounds/bi_14th.wav b/resources/sounds/bi_14th.wav new file mode 100644 index 0000000..f402c8f Binary files /dev/null and b/resources/sounds/bi_14th.wav differ diff --git a/resources/sounds/bi_15.wav b/resources/sounds/bi_15.wav new file mode 100644 index 0000000..71e66fa Binary files /dev/null and b/resources/sounds/bi_15.wav differ diff --git a/resources/sounds/bi_15th.wav b/resources/sounds/bi_15th.wav new file mode 100644 index 0000000..a94efd0 Binary files /dev/null and b/resources/sounds/bi_15th.wav differ diff --git a/resources/sounds/bi_16.wav b/resources/sounds/bi_16.wav new file mode 100644 index 0000000..1e2d438 Binary files /dev/null and b/resources/sounds/bi_16.wav differ diff --git a/resources/sounds/bi_16th.wav b/resources/sounds/bi_16th.wav new file mode 100644 index 0000000..2c91d9b Binary files /dev/null and b/resources/sounds/bi_16th.wav differ diff --git a/resources/sounds/bi_17.wav b/resources/sounds/bi_17.wav new file mode 100644 index 0000000..6516848 Binary files /dev/null and b/resources/sounds/bi_17.wav differ diff --git a/resources/sounds/bi_17th.wav b/resources/sounds/bi_17th.wav new file mode 100644 index 0000000..deb404b Binary files /dev/null and b/resources/sounds/bi_17th.wav differ diff --git a/resources/sounds/bi_18.wav b/resources/sounds/bi_18.wav new file mode 100644 index 0000000..15c819d Binary files /dev/null and b/resources/sounds/bi_18.wav differ diff --git a/resources/sounds/bi_18th.wav b/resources/sounds/bi_18th.wav new file mode 100644 index 0000000..9e20df4 Binary files /dev/null and b/resources/sounds/bi_18th.wav differ diff --git a/resources/sounds/bi_19.wav b/resources/sounds/bi_19.wav new file mode 100644 index 0000000..4769bfd Binary files /dev/null and b/resources/sounds/bi_19.wav differ diff --git a/resources/sounds/bi_19th.wav b/resources/sounds/bi_19th.wav new file mode 100644 index 0000000..5e7d5b0 Binary files /dev/null and b/resources/sounds/bi_19th.wav differ diff --git a/resources/sounds/bi_1f.wav b/resources/sounds/bi_1f.wav new file mode 100644 index 0000000..b93fcf5 Binary files /dev/null and b/resources/sounds/bi_1f.wav differ diff --git a/resources/sounds/bi_1n.wav b/resources/sounds/bi_1n.wav new file mode 100644 index 0000000..b93fcf5 Binary files /dev/null and b/resources/sounds/bi_1n.wav differ diff --git a/resources/sounds/bi_1th.wav b/resources/sounds/bi_1th.wav new file mode 100644 index 0000000..ab27ce0 Binary files /dev/null and b/resources/sounds/bi_1th.wav differ diff --git a/resources/sounds/bi_2.wav b/resources/sounds/bi_2.wav new file mode 100644 index 0000000..dc5002d Binary files /dev/null and b/resources/sounds/bi_2.wav differ diff --git a/resources/sounds/bi_20.wav b/resources/sounds/bi_20.wav new file mode 100644 index 0000000..3711da5 Binary files /dev/null and b/resources/sounds/bi_20.wav differ diff --git a/resources/sounds/bi_2000.wav b/resources/sounds/bi_2000.wav new file mode 100644 index 0000000..f6f4fe8 Binary files /dev/null and b/resources/sounds/bi_2000.wav differ diff --git a/resources/sounds/bi_20th.wav b/resources/sounds/bi_20th.wav new file mode 100644 index 0000000..c364b69 Binary files /dev/null and b/resources/sounds/bi_20th.wav differ diff --git a/resources/sounds/bi_21.wav b/resources/sounds/bi_21.wav new file mode 100644 index 0000000..8f8c422 Binary files /dev/null and b/resources/sounds/bi_21.wav differ diff --git a/resources/sounds/bi_21th.wav b/resources/sounds/bi_21th.wav new file mode 100644 index 0000000..363724a Binary files /dev/null and b/resources/sounds/bi_21th.wav differ diff --git a/resources/sounds/bi_22.wav b/resources/sounds/bi_22.wav new file mode 100644 index 0000000..2457973 Binary files /dev/null and b/resources/sounds/bi_22.wav differ diff --git a/resources/sounds/bi_22th.wav b/resources/sounds/bi_22th.wav new file mode 100644 index 0000000..c1df552 Binary files /dev/null and b/resources/sounds/bi_22th.wav differ diff --git a/resources/sounds/bi_23.wav b/resources/sounds/bi_23.wav new file mode 100644 index 0000000..d17bc31 Binary files /dev/null and b/resources/sounds/bi_23.wav differ diff --git a/resources/sounds/bi_23th.wav b/resources/sounds/bi_23th.wav new file mode 100644 index 0000000..5b32266 Binary files /dev/null and b/resources/sounds/bi_23th.wav differ diff --git a/resources/sounds/bi_24.wav b/resources/sounds/bi_24.wav new file mode 100644 index 0000000..b56e5fa Binary files /dev/null and b/resources/sounds/bi_24.wav differ diff --git a/resources/sounds/bi_24th.wav b/resources/sounds/bi_24th.wav new file mode 100644 index 0000000..81b5230 Binary files /dev/null and b/resources/sounds/bi_24th.wav differ diff --git a/resources/sounds/bi_25th.wav b/resources/sounds/bi_25th.wav new file mode 100644 index 0000000..f00211e Binary files /dev/null and b/resources/sounds/bi_25th.wav differ diff --git a/resources/sounds/bi_26th.wav b/resources/sounds/bi_26th.wav new file mode 100644 index 0000000..f5f80e0 Binary files /dev/null and b/resources/sounds/bi_26th.wav differ diff --git a/resources/sounds/bi_27th.wav b/resources/sounds/bi_27th.wav new file mode 100644 index 0000000..4f47d82 Binary files /dev/null and b/resources/sounds/bi_27th.wav differ diff --git a/resources/sounds/bi_28th.wav b/resources/sounds/bi_28th.wav new file mode 100644 index 0000000..7b0e9e6 Binary files /dev/null and b/resources/sounds/bi_28th.wav differ diff --git a/resources/sounds/bi_29th.wav b/resources/sounds/bi_29th.wav new file mode 100644 index 0000000..1f45b93 Binary files /dev/null and b/resources/sounds/bi_29th.wav differ diff --git a/resources/sounds/bi_2th.wav b/resources/sounds/bi_2th.wav new file mode 100644 index 0000000..88f72af Binary files /dev/null and b/resources/sounds/bi_2th.wav differ diff --git a/resources/sounds/bi_3.wav b/resources/sounds/bi_3.wav new file mode 100644 index 0000000..96f1c1a Binary files /dev/null and b/resources/sounds/bi_3.wav differ diff --git a/resources/sounds/bi_30.wav b/resources/sounds/bi_30.wav new file mode 100644 index 0000000..eb57682 Binary files /dev/null and b/resources/sounds/bi_30.wav differ diff --git a/resources/sounds/bi_30th.wav b/resources/sounds/bi_30th.wav new file mode 100644 index 0000000..0bee7a7 Binary files /dev/null and b/resources/sounds/bi_30th.wav differ diff --git a/resources/sounds/bi_31th.wav b/resources/sounds/bi_31th.wav new file mode 100644 index 0000000..1edc0ea Binary files /dev/null and b/resources/sounds/bi_31th.wav differ diff --git a/resources/sounds/bi_3th.wav b/resources/sounds/bi_3th.wav new file mode 100644 index 0000000..5bde456 Binary files /dev/null and b/resources/sounds/bi_3th.wav differ diff --git a/resources/sounds/bi_4.wav b/resources/sounds/bi_4.wav new file mode 100644 index 0000000..f074642 Binary files /dev/null and b/resources/sounds/bi_4.wav differ diff --git a/resources/sounds/bi_40.wav b/resources/sounds/bi_40.wav new file mode 100644 index 0000000..8e794ad Binary files /dev/null and b/resources/sounds/bi_40.wav differ diff --git a/resources/sounds/bi_4th.wav b/resources/sounds/bi_4th.wav new file mode 100644 index 0000000..2fb2d2c Binary files /dev/null and b/resources/sounds/bi_4th.wav differ diff --git a/resources/sounds/bi_5.wav b/resources/sounds/bi_5.wav new file mode 100644 index 0000000..14eb7d6 Binary files /dev/null and b/resources/sounds/bi_5.wav differ diff --git a/resources/sounds/bi_50.wav b/resources/sounds/bi_50.wav new file mode 100644 index 0000000..ec6fa76 Binary files /dev/null and b/resources/sounds/bi_50.wav differ diff --git a/resources/sounds/bi_5th.wav b/resources/sounds/bi_5th.wav new file mode 100644 index 0000000..71e1816 Binary files /dev/null and b/resources/sounds/bi_5th.wav differ diff --git a/resources/sounds/bi_6.wav b/resources/sounds/bi_6.wav new file mode 100644 index 0000000..ce319e8 Binary files /dev/null and b/resources/sounds/bi_6.wav differ diff --git a/resources/sounds/bi_60.wav b/resources/sounds/bi_60.wav new file mode 100644 index 0000000..a9dbe85 Binary files /dev/null and b/resources/sounds/bi_60.wav differ diff --git a/resources/sounds/bi_6th.wav b/resources/sounds/bi_6th.wav new file mode 100644 index 0000000..b8760c2 Binary files /dev/null and b/resources/sounds/bi_6th.wav differ diff --git a/resources/sounds/bi_7.wav b/resources/sounds/bi_7.wav new file mode 100644 index 0000000..4cf0795 Binary files /dev/null and b/resources/sounds/bi_7.wav differ diff --git a/resources/sounds/bi_70.wav b/resources/sounds/bi_70.wav new file mode 100644 index 0000000..3bd05c4 Binary files /dev/null and b/resources/sounds/bi_70.wav differ diff --git a/resources/sounds/bi_7th.wav b/resources/sounds/bi_7th.wav new file mode 100644 index 0000000..65524a9 Binary files /dev/null and b/resources/sounds/bi_7th.wav differ diff --git a/resources/sounds/bi_8.wav b/resources/sounds/bi_8.wav new file mode 100644 index 0000000..0ea1a2d Binary files /dev/null and b/resources/sounds/bi_8.wav differ diff --git a/resources/sounds/bi_80.wav b/resources/sounds/bi_80.wav new file mode 100644 index 0000000..e7a971d Binary files /dev/null and b/resources/sounds/bi_80.wav differ diff --git a/resources/sounds/bi_8th.wav b/resources/sounds/bi_8th.wav new file mode 100644 index 0000000..1223afb Binary files /dev/null and b/resources/sounds/bi_8th.wav differ diff --git a/resources/sounds/bi_9.wav b/resources/sounds/bi_9.wav new file mode 100644 index 0000000..002efbd Binary files /dev/null and b/resources/sounds/bi_9.wav differ diff --git a/resources/sounds/bi_90.wav b/resources/sounds/bi_90.wav new file mode 100644 index 0000000..70c7fdf Binary files /dev/null and b/resources/sounds/bi_90.wav differ diff --git a/resources/sounds/bi_9th.wav b/resources/sounds/bi_9th.wav new file mode 100644 index 0000000..1612c61 Binary files /dev/null and b/resources/sounds/bi_9th.wav differ diff --git a/resources/sounds/co_enter_pin.wav b/resources/sounds/co_enter_pin.wav new file mode 100644 index 0000000..79468d5 Binary files /dev/null and b/resources/sounds/co_enter_pin.wav differ diff --git a/resources/sounds/co_join.wav b/resources/sounds/co_join.wav new file mode 100644 index 0000000..ddbc7e3 Binary files /dev/null and b/resources/sounds/co_join.wav differ diff --git a/resources/sounds/co_more_participants.wav b/resources/sounds/co_more_participants.wav new file mode 100644 index 0000000..148b65b Binary files /dev/null and b/resources/sounds/co_more_participants.wav differ diff --git a/resources/sounds/co_only_one.wav b/resources/sounds/co_only_one.wav new file mode 100644 index 0000000..116dba1 Binary files /dev/null and b/resources/sounds/co_only_one.wav differ diff --git a/resources/sounds/co_right_pin.wav b/resources/sounds/co_right_pin.wav new file mode 100644 index 0000000..05decdb Binary files /dev/null and b/resources/sounds/co_right_pin.wav differ diff --git a/resources/sounds/co_say_name.wav b/resources/sounds/co_say_name.wav new file mode 100644 index 0000000..35d0e56 Binary files /dev/null and b/resources/sounds/co_say_name.wav differ diff --git a/resources/sounds/co_there_are.wav b/resources/sounds/co_there_are.wav new file mode 100644 index 0000000..0dd48c5 Binary files /dev/null and b/resources/sounds/co_there_are.wav differ diff --git a/resources/sounds/co_there_is.wav b/resources/sounds/co_there_is.wav new file mode 100644 index 0000000..8008084 Binary files /dev/null and b/resources/sounds/co_there_is.wav differ diff --git a/resources/sounds/co_welcome_conference.wav b/resources/sounds/co_welcome_conference.wav new file mode 100644 index 0000000..d2fc042 Binary files /dev/null and b/resources/sounds/co_welcome_conference.wav differ diff --git a/resources/sounds/co_wrong_pin.wav b/resources/sounds/co_wrong_pin.wav new file mode 100644 index 0000000..4aa1b42 Binary files /dev/null and b/resources/sounds/co_wrong_pin.wav differ diff --git a/resources/sounds/moh/Cold_Day_vbr.wav b/resources/sounds/moh/Cold_Day_vbr.wav new file mode 100644 index 0000000..e23d451 Binary files /dev/null and b/resources/sounds/moh/Cold_Day_vbr.wav differ diff --git a/resources/sounds/moh/Cool_Grass_vbr.wav b/resources/sounds/moh/Cool_Grass_vbr.wav new file mode 100644 index 0000000..2cffe68 Binary files /dev/null and b/resources/sounds/moh/Cool_Grass_vbr.wav differ diff --git a/resources/sounds/moh/Listening_to_the_Birds_vbr.wav b/resources/sounds/moh/Listening_to_the_Birds_vbr.wav new file mode 100644 index 0000000..44efa97 Binary files /dev/null and b/resources/sounds/moh/Listening_to_the_Birds_vbr.wav differ diff --git a/resources/sounds/moh/Macroform-Letting_Yourself_Go-05-Letting_Yourself_Go_vbr.wav b/resources/sounds/moh/Macroform-Letting_Yourself_Go-05-Letting_Yourself_Go_vbr.wav new file mode 100644 index 0000000..6658c7d Binary files /dev/null and b/resources/sounds/moh/Macroform-Letting_Yourself_Go-05-Letting_Yourself_Go_vbr.wav differ diff --git a/resources/sounds/moh/Manolo Camp - morningcoffee.wav b/resources/sounds/moh/Manolo Camp - morningcoffee.wav new file mode 100644 index 0000000..50362f3 Binary files /dev/null and b/resources/sounds/moh/Manolo Camp - morningcoffee.wav differ diff --git a/resources/sounds/moh/dokapi-bonus_tracks-01-space_odyssey_ambient.wav b/resources/sounds/moh/dokapi-bonus_tracks-01-space_odyssey_ambient.wav new file mode 100644 index 0000000..8cee19b Binary files /dev/null and b/resources/sounds/moh/dokapi-bonus_tracks-01-space_odyssey_ambient.wav differ diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..8b99e9f --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/python + +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details +# + +import glob +import os + +from distutils.core import setup + +from sylk import __version__ + + +def find_packages(toplevel): + return [directory.replace(os.path.sep, '.') for directory, subdirs, files in os.walk(toplevel) if '__init__.py' in files] + +setup(name = "sylkserver", + version = __version__, + author = "AG Projects", + author_email = "support@ag-projects.com", + url = "http://sylkserver.com", + description = "SylkServer - A state of the art, extensible SIP Application Server", + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Service Providers", + "License :: GNU Lesser General Public License 3", + "Operating System :: OS Independent", + "Programming Language :: Python" + ], + packages = find_packages('sylk'), + scripts = ['sylk-server'], + data_files = [('/etc/sylkserver/tls', []), + ('/var/lib/sylkserver', []), + ('share/sylkserver/sounds', glob.glob(os.path.join('resources', 'sounds', '*.wav'))), + ('share/sylkserver/sounds/moh', glob.glob(os.path.join('resources', 'sounds', 'moh','*.wav')))] + ) + diff --git a/sylk-server b/sylk-server new file mode 100755 index 0000000..91e8ef1 --- /dev/null +++ b/sylk-server @@ -0,0 +1,113 @@ +#!/usr/bin/env python + +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details +# + +import os +import signal +import sys + +from optparse import OptionParser + +from application import log +from application.process import process, ProcessError + +import sylk + + +DEBUG = False + +def stop_server(*args): + from sylk.server import SylkServer + log.msg('Stopping SylkServer...') + server = SylkServer() + server.stop() + +def main(): + name = 'sylk-server' + fullname = 'SylkServer' + runtime_directory = '/var/run/sylkserver' + system_config_directory = '/etc/sylkserver' + default_pid = os.path.join(runtime_directory, 'server.pid') + default_config = os.path.join(system_config_directory , sylk.configuration_filename) + + parser = OptionParser(version='%%prog %s' % sylk.__version__) + parser.add_option('--no-fork', action='store_false', dest='fork', default=1, + help='run the process in the foreground (for debugging)') + parser.add_option('--pid', dest='pid_file', + help='pid file ("%s")' % default_pid, metavar='File') + parser.add_option('--config-file', dest='config_file', default=default_config, + help='path to configuration file to read ("%s")' % default_config, + metavar='File') + (options, args) = parser.parse_args() + + try: + sylk.dependencies.check() + except Exception, e: + log.fatal(str(e)) + sys.exit(1) + + path, configuration_filename = os.path.split(options.config_file) + if path: + system_config_directory = path + + process.system_config_directory = system_config_directory + sylk.configuration_filename = process.config_file(configuration_filename) + + # when run in foreground, do not require root access because of /var/run/sylkserver + if not options.fork: + process._runtime_directory = None + else: + try: + process.runtime_directory = runtime_directory + except ProcessError, e: + log.fatal("Cannot start %s: %s" % (fullname, e)) + sys.exit(1) + + pid_file = options.pid_file or default_pid + + if options.fork: + try: + process.daemonize(pid_file) + except ProcessError, e: + log.fatal("Cannot start %s: %s" % (fullname, e)) + sys.exit(1) + log.start_syslog(name) + + if sylk.configuration_filename: + log.msg("Starting %s %s, config=%s" % (fullname, sylk.__version__, sylk.configuration_filename)) + else: + log.fatal('Config file not found') + sys.exit(1) + + try: + if not options.fork and DEBUG: + from application.debug.memory import memory_dump + from sylk.server import SylkServer + server = SylkServer() + except Exception, e: + log.fatal("failed to create %s: %s" % (fullname, e)) + log.err() + sys.exit(1) + + process.signals.add_handler(signal.SIGTERM, stop_server) + process.signals.add_handler(signal.SIGINT, stop_server) + + try: + server.start() + signal.pause() + server.stop_event.wait() + log.msg("%s stopped" % fullname) + except Exception, e: + log.fatal("failed to run %s: %s" % (fullname, e)) + log.err() + sys.exit(1) + + if not options.fork and DEBUG: + memory_dump() + + +if __name__ == "__main__": + main() + + diff --git a/sylk/__init__.py b/sylk/__init__.py new file mode 100644 index 0000000..065cf99 --- /dev/null +++ b/sylk/__init__.py @@ -0,0 +1,25 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details + +"""SylkServer""" + +__version__ = '1.0.0' + +configuration_filename = "config.ini" + + +package_requirements = {'python-application': '1.2.4', + 'python-sipsimple': '0.17.0'} + +try: + from application.dependency import ApplicationDependencies +except: + class DependencyError(Exception): pass + class ApplicationDependencies(object): + def __init__(self, *args, **kwargs): + pass + def check(self): + raise DependencyError("need python-application version %s or higher but it's not installed" % package_requirements['python-application']) + +dependencies = ApplicationDependencies(**package_requirements) + + diff --git a/sylk/applications/__init__.py b/sylk/applications/__init__.py new file mode 100644 index 0000000..b56d729 --- /dev/null +++ b/sylk/applications/__init__.py @@ -0,0 +1,126 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details +# + +__all__ = ['ISylkApplication', 'ApplicationRegistry', 'sylk_application', 'IncomingRequestHandler'] + +import os + +from application import log +from application.notification import IObserver, NotificationCenter +from application.python.util import Null, Singleton +from sipsimple.threading import run_in_twisted_thread +from zope.interface import Attribute, Interface, implements + +from sylk.configuration import ServerConfig + + +class ISylkApplication(Interface): + """ + Interface defining attributes and methods any application must + implement. + + Each application must be a Singleton and has to be decorated with + the @sylk_application decorator. + """ + + __appname__ = Attribute("Application name") + + def incoming_session(self, session): + pass + + def incoming_subscription(self, subscribe_request, data): + pass + + def incoming_sip_message(self, message_request, data): + pass + + +class ApplicationRegistry(object): + __metaclass__ = Singleton + + def __init__(self): + self.applications = [] + + def __iter__(self): + return iter(self.applications) + + def add(self, app): + if app not in self.applications: + self.applications.append(app) + + +def sylk_application(cls): + """Class decorator for adding applications to the ApplicationRegistry""" + ApplicationRegistry().add(cls()) + return cls + + +def load_applications(): + toplevel = os.path.dirname(__file__) + app_list = ['sylk.applications.%s' % item for item in os.listdir(toplevel) if os.path.isdir(os.path.join(toplevel, item)) and '__init__.py' in os.listdir(os.path.join(toplevel, item))] + map(__import__, app_list) + + +class IncomingRequestHandler(object): + """ + Handle incoming requests and match them to applications. + """ + __metaclass__ = Singleton + implements(IObserver) + + # TODO: implement a 'find_application' function which will get the appropriate application + # as defined in the configuration + # TODO: apply ACLs (before or after?) + def __init__(self): + load_applications() + log.msg('Loaded applications: %s' % ','.join([app.__appname__ for app in ApplicationRegistry()])) + + def start(self): + notification_center = NotificationCenter() + notification_center.add_observer(self, name='SIPSessionNewIncoming') + notification_center.add_observer(self, name='SIPIncomingSubscriptionGotSubscribe') + notification_center.add_observer(self, name='SIPIncomingRequestGotRequest') + + def stop(self): + notification_center = NotificationCenter() + notification_center.remove_observer(self, name='SIPSessionNewIncoming') + notification_center.remove_observer(self, name='SIPIncomingSubscriptionGotSubscribe') + notification_center.remove_observer(self, name='SIPIncomingRequestGotRequest') + + @run_in_twisted_thread + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + def _NH_SIPSessionNewIncoming(self, notification): + session = notification.sender + try: + app = (app for app in ApplicationRegistry() if app.__appname__ == ServerConfig.default_application).next() + except StopIteration: + pass + else: + app.incoming_session(session) + + def _NH_SIPIncomingSubscriptionGotSubscribe(self, notification): + subscribe_request = notification.sender + try: + app = (app for app in ApplicationRegistry() if app.__appname__ == ServerConfig.default_application).next() + except StopIteration: + pass + else: + app.incoming_subscription(subscribe_request, notification.data) + + def _NH_SIPIncomingRequestGotRequest(self, notification): + request = notification.sender + if notification.data.method != 'MESSAGE': + request.answer(405) + return + try: + app = (app for app in ApplicationRegistry() if app.__appname__ == ServerConfig.default_application).next() + except StopIteration: + pass + else: + app.incoming_sip_message(request, notification.data) + + + diff --git a/sylk/applications/conference/__init__.py b/sylk/applications/conference/__init__.py new file mode 100644 index 0000000..dc99035 --- /dev/null +++ b/sylk/applications/conference/__init__.py @@ -0,0 +1,103 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details +# + +from application import log +from application.notification import IObserver, NotificationCenter +from application.python.util import Null, Singleton +from twisted.internet import reactor +from zope.interface import implements + +from sylk.applications import ISylkApplication, sylk_application +from sylk.applications.conference.configuration import ConferenceConfig +from sylk.applications.conference.room import Room + +# Initialize database +from sylk.applications.conference import database + + +@sylk_application +class ConferenceApplication(object): + __metaclass__ = Singleton + implements(ISylkApplication, IObserver) + + __appname__ = 'conference' + + def __init__(self): + self.rooms = set() + self.pending_sessions = [] + + def incoming_session(self, session): + log.msg('New incoming session from %s' % session.remote_identity.uri) + audio_streams = [stream for stream in session.proposed_streams if stream.type=='audio'] + chat_streams = [stream for stream in session.proposed_streams if stream.type=='chat'] + if not audio_streams and not chat_streams: + session.reject(488) + return + self.pending_sessions.append(session) + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=session) + if audio_streams: + session.send_ring_indication() + streams = [streams[0] for streams in (audio_streams, chat_streams) if streams] + reactor.callLater(4 if audio_streams else 0, self.accept_session, session, streams) + + def incoming_subscription(self, subscribe_request, data): + to_header = data.headers.get('To', Null) + if to_header is Null: + subscribe_request.reject(400) + return + room = Room.get_room(data.request_uri) + if not room.started: + room = Room.get_room(to_header.uri) + if not room.started: + subscribe_request.reject(480) + return + room.handle_incoming_subscription(subscribe_request, data) + + def incoming_sip_message(self, message_request, data): + if not ConferenceConfig.enable_sip_message: + return + room = Room.get_room(data.request_uri) + if not room.started: + message_request.answer(480) + return + room.handle_incoming_sip_message(message_request, data) + + def accept_session(self, session, streams): + if session in self.pending_sessions: + session.accept(streams, is_focus=True) + + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + def _NH_SIPSessionDidStart(self, notification): + session = notification.sender + self.pending_sessions.remove(session) + room = Room.get_room(session.local_identity.uri) + room.start() + room.add_session(session) + self.rooms.add(room) + + def _NH_SIPSessionDidEnd(self, notification): + session = notification.sender + log.msg('Session from %s ended' % session.remote_identity.uri) + notification_center = NotificationCenter() + notification_center.remove_observer(self, sender=session) + room = Room.get_room(session.local_identity.uri) + if session in room.sessions: + # We could get this notifiction even if we didn't get SIPSessionDidStart + room.remove_session(session) + if room.empty: + room.stop() + try: + self.rooms.remove(room) + except KeyError: + pass + + def _NH_SIPSessionDidFail(self, notification): + session = notification.sender + self.pending_sessions.remove(session) + log.msg('Session from %s failed' % session.remote_identity.uri) + + diff --git a/sylk/applications/conference/configuration.py b/sylk/applications/conference/configuration.py new file mode 100644 index 0000000..bfb5cb9 --- /dev/null +++ b/sylk/applications/conference/configuration.py @@ -0,0 +1,16 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +from application.configuration import ConfigSection, ConfigSetting + + +class ConferenceConfig(ConfigSection): + __cfgfile__ = 'conference.ini' + __section__ = 'Conference' + + db_uri = ConfigSetting(type=str, value='sqlite:///var/lib/sylkserver/conference.sqlite') + history_table = ConfigSetting(type=str, value='message_history') + enable_sip_message = False + replay_history = 20 + + diff --git a/sylk/applications/conference/database.py b/sylk/applications/conference/database.py new file mode 100644 index 0000000..efa441f --- /dev/null +++ b/sylk/applications/conference/database.py @@ -0,0 +1,59 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +__all__ = ['async_save_message', 'get_last_messages'] + +import datetime + +from application.python.util import Null +from eventlet.twistedutil import block_on +from sqlobject import SQLObject, DateTimeCol, UnicodeCol +from twisted.internet.threads import deferToThread + +from sylk.applications.conference.configuration import ConferenceConfig +from sylk.database import Database + + +db = Database(ConferenceConfig.db_uri) + + +class MessageHistory(SQLObject): + class sqlmeta: + table = ConferenceConfig.history_table + _connection = db.connection + date = DateTimeCol() + room_uri = UnicodeCol() + sip_from = UnicodeCol() + cpim_body = UnicodeCol(sqlType='LONGTEXT') + cpim_content_type = UnicodeCol() + cpim_sender = UnicodeCol() + cpim_recipient = UnicodeCol() + cpim_timestamp = DateTimeCol() + +db.create_table(MessageHistory) + + +def _save_message(sip_from, room_uri, cpim_body, cpim_content_type, cpim_sender, cpim_recipient, cpim_timestamp): + return MessageHistory(date = datetime.datetime.utcnow(), + room_uri = room_uri, + sip_from = sip_from, + cpim_body = cpim_body, + cpim_content_type = cpim_content_type, + cpim_sender = cpim_sender, + cpim_recipient = cpim_recipient, + cpim_timestamp = cpim_timestamp) + +def async_save_message(sip_from, room_uri, cpim_body, cpim_content_type, cpim_sender, cpim_recipient, cpim_timestamp): + if db.connection is not Null: + return deferToThread(_save_message, sip_from, room_uri, cpim_body, cpim_content_type, cpim_sender, cpim_recipient, cpim_timestamp) + +def _get_last_messages(room_uri, count): + return list(MessageHistory.selectBy(room_uri=room_uri)[-count:]) + +def get_last_messages(room_uri, count): + if db.connection is not Null and count > 0: + return block_on(deferToThread(_get_last_messages, room_uri, count)) + else: + return [] + + diff --git a/sylk/applications/conference/room.py b/sylk/applications/conference/room.py new file mode 100644 index 0000000..c4e3f94 --- /dev/null +++ b/sylk/applications/conference/room.py @@ -0,0 +1,652 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +import random + +from datetime import datetime +from glob import glob +from itertools import cycle +from time import mktime + +from application import log +from application.notification import IObserver, NotificationCenter +from application.python.util import Null, Singleton +from eventlet import coros, proc +from sipsimple.application import SIPApplication +from sipsimple.audio import WavePlayer, WavePlayerError +from sipsimple.conference import AudioConference +from sipsimple.configuration.settings import SIPSimpleSettings +from sipsimple.core import FromHeader, ToHeader, RouteHeader, SIPURI, Message, SIPCoreInvalidStateError +from sipsimple.lookup import DNSLookup, DNSLookupError +from sipsimple.payloads.conference import Conference, ConferenceDescription, ConferenceState, Endpoint, EndpointStatus, HostInfo, JoiningInfo, Media, User, Users, WebPage +from sipsimple.payloads.iscomposing import IsComposingMessage, State, LastActive, Refresh, ContentType +from sipsimple.streams.applications.chat import CPIMIdentity, CPIMMessage, CPIMParserError +from sipsimple.streams.msrp import ChatStreamError +from sipsimple.threading import run_in_twisted_thread +from sipsimple.threading.green import run_in_green_thread +from sipsimple.util import Timestamp +from twisted.internet import reactor +from zope.interface import implements + +from sylk.applications.conference import database +from sylk.applications.conference.configuration import ConferenceConfig +from sylk.configuration.datatypes import ResourcePath + + +def format_identity(identity, cpim_format=False): + uri = identity.uri + if identity.display_name: + return '%s ' % (identity.display_name, uri.user, uri.host) + elif cpim_format: + return '' % (uri.user, uri.host) + else: + return 'sip:%s@%s' % (uri.user, uri.host) + +def format_stream_types(streams): + if not streams: + return '' + if len(streams) == 1: + txt = 'with %s' % streams[0].type + else: + txt = 'with %s' % ','.join(stream.type for stream in streams[:-1]) + txt += ' and %s' % streams[-1:][0].type + return txt + +def format_conference_stream_type(stream): + if stream.type == 'chat': + return 'message' + return stream.type + +def format_identity_with_stream_types(identity, streams): + return '%s %s' % (format_identity(identity), format_stream_types(streams)) + +def format_session_duration(session): + if session.start_time: + duration = session.end_time - session.start_time + seconds = duration.seconds if duration.microseconds < 500000 else duration.seconds+1 + minutes, seconds = seconds / 60, seconds % 60 + hours, minutes = minutes / 60, minutes % 60 + hours += duration.days*24 + if not minutes and not hours: + duration_text = '%d seconds' % seconds + elif not hours: + duration_text = '%02d:%02d' % (minutes, seconds) + else: + duration_text = '%02d:%02d:%02d' % (hours, minutes, seconds) + else: + duration_text = '0s' + return duration_text + +def chunks(text, size): + for i in xrange(0, len(text), size): + yield text[i:i+size] + + +class SIPMessage(object): + def __init__(self, sender, recipient, content_type, body): + self.sender = sender + self.recipient = recipient + self.content_type = content_type + self.body = body + self.timestamp = None + + +class Room(object): + """ + Object representing a conference room, it will handle the message dispatching + among all the participants. + """ + __metaclass__ = Singleton + implements(IObserver) + + def __init__(self, uri): + self.uri = uri + self.identity = CPIMIdentity.parse('<%s>' % self.uri) + self.sessions = [] + self.sessions_with_proposals = [] + self.subscriptions = [] + self.state = 'stopped' + self.incoming_message_queue = coros.queue() + self.message_dispatcher = None + self.audio_conference = None + self.moh_player = None + self.conference_info_payload = None + + @classmethod + def get_room(cls, uri): + room_uri = '%s@%s' % (uri.user, uri.host) + room = cls(room_uri) + return room + + @property + def empty(self): + return len(self.sessions) == 0 + + @property + def started(self): + return self.state == 'started' + + def start(self): + if self.state != 'stopped': + return + self.message_dispatcher = proc.spawn(self._message_dispatcher) + self.audio_conference = AudioConference() + self.audio_conference.hold() + self.moh_player = MoHPlayer(self.audio_conference) + self.moh_player.initialize() + self.state = 'started' + + def stop(self): + if self.state != 'started': + return + self.state = 'stopped' + self.message_dispatcher.kill(proc.ProcExit) + self.moh_player = None + self.audio_conference = None + + def _message_dispatcher(self): + """Read from self.incoming_message_queue and dispatch the messages to other participants""" + while True: + session, message_type, data = self.incoming_message_queue.wait() + if message_type == 'message': + if data.timestamp is not None and isinstance(data.timestamp, Timestamp): + timestamp = datetime.fromtimestamp(mktime(data.timestamp.timetuple())) + else: + timestamp = datetime.now() + if data.sender.uri != session.remote_identity.uri: + return + recipient = data.recipients[0] + database.async_save_message(format_identity(session.remote_identity, True), self.uri, data.body, data.content_type, unicode(data.sender), unicode(recipient), timestamp) + if recipient.uri == self.identity.uri: + self.dispatch_message(session, data) + else: + self.dispatch_private_message(session, data) + elif message_type == 'sip_message': + database.async_save_message(format_identity(session.remote_identity, True), self.uri, data.body, data.content_type, unicode(data.sender), data.recipient, data.timestamp) + self.dispatch_message(session, data) + elif message_type == 'composing_indication': + if data.sender.uri != session.remote_identity.uri: + return + recipient = data.recipients[0] + if recipient.uri == self.identity.uri: + self.dispatch_iscomposing(session, data) + else: + self.dispatch_private_iscomposing(session, data) + + def dispatch_message(self, session, message): + for s in (s for s in self.sessions if s is not session): + try: + identity = CPIMIdentity.parse(format_identity(session.remote_identity, True)) + chat_stream = (stream for stream in s.streams if stream.type == 'chat').next() + chat_stream.send_message(message.body, message.content_type, local_identity=identity, recipients=[self.identity], timestamp=message.timestamp) + except ChatStreamError, e: + log.error('Error dispatching message to %s: %s' % (s.remote_identity.uri, e)) + except StopIteration: + # This session doesn't have a chat stream, send him a SIP MESSAGE + if ConferenceConfig.enable_sip_message: + self.send_sip_message(session.remote_identity.uri, s.remote_identity.uri, message.content_type, message.body) + + def dispatch_private_message(self, session, message): + # Private messages are delivered to all sessions matching the recipient but also to the sender, + # for replication in clients + recipient = message.recipients[0] + for s in (s for s in self.sessions if s is not session and s.remote_identity.uri in (recipient.uri, session.remote_identity.uri)): + try: + identity = CPIMIdentity.parse(format_identity(session.remote_identity, True)) + chat_stream = (stream for stream in s.streams if stream.type == 'chat').next() + chat_stream.send_message(message.body, message.content_type, local_identity=identity, recipients=[recipient], timestamp=message.timestamp) + except ChatStreamError, e: + log.error('Error dispatching private message to %s: %s' % (s.remote_identity.uri, e)) + + def dispatch_iscomposing(self, session, data): + for s in (s for s in self.sessions if s is not session): + try: + identity = CPIMIdentity.parse(format_identity(session.remote_identity, True)) + chat_stream = (stream for stream in s.streams if stream.type == 'chat').next() + chat_stream.send_composing_indication(data.state, data.refresh, data.last_active, local_identity=identity, recipients=[self.identity]) + except ChatStreamError, e: + log.error('Error dispatching composing indication to %s: %s' % (s.remote_identity.uri, e)) + except StopIteration: + # This session doesn't have a chat stream, send him a SIP MESSAGE + if ConferenceConfig.enable_sip_message: + body = IsComposingMessage(state=State(data.state), refresh=Refresh(data.refresh), last_active=LastActive(data.last_active or datetime.now()), content_type=ContentType('text')).toxml() + self.send_sip_message(session.remote_identity.uri, s.remote_identity.uri, IsComposingMessage.content_type, body) + + def dispatch_private_iscomposing(self, session, data): + recipient_uri = data.recipients[0].uri + for s in (s for s in self.sessions if s is not session and s.remote_identity.uri == recipient_uri): + try: + identity = CPIMIdentity.parse(format_identity(session.remote_identity, True)) + chat_stream = (stream for stream in s.streams if stream.type == 'chat').next() + chat_stream.send_composing_indication(data.state, data.refresh, data.last_active, local_identity=identity) + except ChatStreamError, e: + log.error('Error dispatching private composing indication to %s: %s' % (s.remote_identity.uri, e)) + + def dispatch_server_message(self, body, content_type='text/plain', exclude=None): + for session in (session for session in self.sessions if session is not exclude): + try: + chat_stream = (stream for stream in session.streams if stream.type == 'chat').next() + chat_stream.send_message(body, content_type, local_identity=self.identity, recipients=[self.identity]) + except StopIteration: + # This session doesn't have a chat stream, send him a SIP MESSAGE + if ConferenceConfig.enable_sip_message: + self.send_sip_message(self.identity.uri, session.remote_identity.uri, content_type, body) + self_identity = format_identity(self.identity, cpim_format=True) + database.async_save_message(self_identity, self.uri, body, content_type, self_identity, self_identity, datetime.now()) + + def dispatch_conference_info(self): + data = self.build_conference_info_payload() + for subscription in (subscription for subscription in self.subscriptions if subscription.state == 'active'): + try: + subscription.push_content(Conference.content_type, data) + except SIPCoreInvalidStateError: + pass + + @run_in_green_thread + def send_sip_message(self, from_uri, to_uri, content_type, body): + lookup = DNSLookup() + settings = SIPSimpleSettings() + try: + routes = lookup.lookup_sip_proxy(to_uri, settings.sip.transport_list).wait() + except DNSLookupError: + log.warning('DNS lookup error while looking for %s proxy' % to_uri) + else: + route = routes.pop(0) + from_header = FromHeader(self.identity.uri) + to_header = ToHeader(SIPURI.new(to_uri)) + route_header = RouteHeader(route.get_uri()) + sender = CPIMIdentity(from_uri) + for chunk in chunks(body, 1000): + msg = CPIMMessage(chunk, content_type, sender=sender, recipients=[self.identity]) + message_request = Message(from_header, to_header, route_header, 'message/cpim', str(msg)) + message_request.send() + + def render_text_welcome(self, session): + txt = 'Welcome to the conference.' + user_count = len(set(str(s.remote_identity.uri) for s in self.sessions) - set([str(session.remote_identity.uri)])) + if user_count == 0: + txt += ' You are the first participant in the room.' + else: + if user_count == 1: + txt += ' There is one more participant in the room.' + else: + txt += ' There are %s more participants in the room.' % user_count + return txt + + def _play_file_in_player(self, player, file, delay): + player.filename = str(file) + player.pause_time = delay + try: + player.play().wait() + except WavePlayerError, e: + log.warning("Error playing file %s: %s" % (file, e)) + + @run_in_green_thread + def play_audio_welcome(self, session, play_welcome=True): + audio_stream = (stream for stream in session.streams if stream.type == 'audio').next() + player = WavePlayer(audio_stream.mixer, '', pause_time=1, initial_play=False, volume=50) + audio_stream.bridge.add(player) + if play_welcome: + file = ResourcePath('sounds/co_welcome_conference.wav').normalized + self._play_file_in_player(player, file, 1) + user_count = len(set(str(s.remote_identity.uri) for s in self.sessions if any(stream for stream in s.streams if stream.type == 'audio')) - set([str(session.remote_identity.uri)])) + if user_count == 0: + file = ResourcePath('sounds/co_only_one.wav').normalized + self._play_file_in_player(player, file, 0.5) + elif user_count == 1: + file = ResourcePath('sounds/co_there_is.wav').normalized + self._play_file_in_player(player, file, 0.5) + elif user_count < 100: + file = ResourcePath('sounds/co_there_are.wav').normalized + self._play_file_in_player(player, file, 0.2) + if user_count <= 24: + file = ResourcePath('sounds/bi_%d.wav' % user_count).normalized + self._play_file_in_player(player, file, 0.1) + else: + file = ResourcePath('sounds/bi_%d0.wav' % (user_count / 10)).normalized + self._play_file_in_player(player, file, 0.1) + file = ResourcePath('sounds/bi_%d.wav' % (user_count % 10)).normalized + self._play_file_in_player(player, file, 0.1) + file = ResourcePath('sounds/co_more_participants.wav').normalized + self._play_file_in_player(player, file, 0) + audio_stream.bridge.remove(player) + self.audio_conference.add(audio_stream) + self.audio_conference.unhold() + if len(self.audio_conference.streams) == 1: + # Play MoH + self.moh_player.play() + else: + self.moh_player.pause() + + def add_session(self, session): + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=session) + self.sessions.append(session) + try: + chat_stream = (stream for stream in session.streams if stream.type == 'chat').next() + except StopIteration: + pass + else: + notification_center.add_observer(self, sender=chat_stream) + remote_identity = CPIMIdentity.parse(format_identity(session.remote_identity, cpim_format=True)) + # getting last messages may take time, so new messages can arrive before messages the last message from history + for msg in database.get_last_messages(self.uri, ConferenceConfig.replay_history): + recipient = CPIMIdentity.parse(msg.cpim_recipient) + sender = CPIMIdentity.parse(msg.cpim_sender) + if recipient.uri in (self.identity.uri, remote_identity.uri) or sender.uri == remote_identity.uri: + chat_stream.send_message(msg.cpim_body, msg.cpim_content_type, local_identity=sender, recipients=[recipient], timestamp=msg.cpim_timestamp) + try: + audio_stream = (stream for stream in session.streams if stream.type == 'audio').next() + except StopIteration: + pass + else: + notification_center.add_observer(self, sender=audio_stream) + log.msg('Audio stream using %s/%sHz (%s), end-points: %s:%d <-> %s:%d' % (audio_stream.codec, audio_stream.sample_rate, + 'encrypted' if audio_stream.srtp_active else 'unencrypted', + audio_stream.local_rtp_address, audio_stream.local_rtp_port, + audio_stream.remote_rtp_address, audio_stream.remote_rtp_port)) + self.play_audio_welcome(session) + self.dispatch_conference_info() + if len(self.sessions) == 1: + log.msg('%s started conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams))) + else: + log.msg('%s joined conference %s %s' % (format_identity(session.remote_identity), self.uri, format_stream_types(session.streams))) + if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session): + self.dispatch_server_message('%s has joined the room %s' % (format_identity(session.remote_identity), format_stream_types(session.streams)), exclude=session) + + def remove_session(self, session): + notification_center = NotificationCenter() + try: + chat_stream = (stream for stream in session.streams or [] if stream.type == 'chat').next() + except StopIteration: + pass + else: + notification_center.remove_observer(self, sender=chat_stream) + try: + audio_stream = (stream for stream in session.streams or [] if stream.type == 'audio').next() + except StopIteration: + pass + else: + notification_center.remove_observer(self, sender=audio_stream) + try: + self.audio_conference.remove(audio_stream) + except ValueError: + # User may hangup before getting bridged into the conference + pass + if len(self.audio_conference.streams) == 0: + self.moh_player.pause() + self.audio_conference.hold() + elif len(self.audio_conference.streams) == 1: + self.moh_player.play() + notification_center.remove_observer(self, sender=session) + self.sessions.remove(session) + self.dispatch_conference_info() + log.msg('%s left conference %s after %s' % (format_identity(session.remote_identity), self.uri, format_session_duration(session))) + if not self.sessions: + log.msg('Last participant left conference %s' % self.uri) + if str(session.remote_identity.uri) not in set(str(s.remote_identity.uri) for s in self.sessions if s is not session): + self.dispatch_server_message('%s has left the room after %s' % (format_identity(session.remote_identity), format_session_duration(session))) + + def handle_incoming_sip_message(self, message_request, data): + content_type = data.headers.get('Content-Type', Null)[0] + from_header = data.headers.get('From', Null) + if content_type is Null or from_header is Null: + message_request.answer(400) + return + try: + # Take the first session which doesn't have a chat stream. This is needed because the + # seession picked up here will later be ignored. It doesn't matter if we ignore a session + # without a chat stream, because that means we will send SIP MESSAGE, and it will fork, so + # everyone will get it. + session = (session for session in self.sessions if str(session.remote_identity.uri) == str(from_header.uri) and any(stream for stream in session.streams if stream.type != 'chat')).next() + except StopIteration: + # MESSAGE from a user which is not in this room + message_request.answer(503) + return + if content_type == 'message/cpim': + try: + message = CPIMMessage.parse(data.body) + except CPIMParserError: + message_request.answer(500) + return + else: + body = message.body + content_type = message.content_type + sender = message.sender or format_identity(from_header, cpim_format=True) + if message.timestamp is not None and isinstance(message.timestamp, Timestamp): + timestamp = datetime.fromtimestamp(mktime(message.timestamp.timetuple())) + else: + timestamp = datetime.now() + else: + body = data.body + sender = format_identity(from_header, cpim_format=True) + timestamp = datetime.now() + message_request.answer(200) + + if content_type == IsComposingMessage.content_type: + return + + log.msg('New incoming MESSAGE from %s' % session.remote_identity.uri) + self_identity = format_identity(self.identity, cpim_format=True) + message = SIPMessage(sender=sender, recipient=self_identity, content_type=content_type, body=body) + message.timestamp = timestamp + self.incoming_message_queue.send((session, 'sip_message', message)) + + def build_conference_info_payload(self): + if self.conference_info_payload is None: + settings = SIPSimpleSettings() + conference_description = ConferenceDescription(display_text='Ad-hoc conference', free_text='Hosted by %s' % settings.user_agent) + host_info = HostInfo(web_page=WebPage('http://sylkserver.com')) + self.conference_info_payload = Conference(self.identity.uri, conference_description=conference_description, host_info=host_info, users=Users()) + user_count = len(set(str(s.remote_identity.uri) for s in self.sessions)) + self.conference_info_payload.conference_state = ConferenceState(user_count=user_count, active=True) + users = Users() + for session in self.sessions: + try: + user = (user for user in users if user.entity == str(session.remote_identity.uri)).next() + except StopIteration: + user = User(str(session.remote_identity.uri), display_text=session.remote_identity.display_name) + users.append(user) + joining_info = JoiningInfo(when=session.start_time) + holdable_streams = [stream for stream in session.streams if stream.hold_supported] + session_on_hold = holdable_streams and all(stream.on_hold_by_remote for stream in holdable_streams) + hold_status = EndpointStatus('on-hold' if session_on_hold else 'connected') + endpoint = Endpoint(str(session._invitation.remote_contact_header.uri), display_text=session.remote_identity.display_name, joining_info=joining_info, status=hold_status) + for stream in session.streams: + endpoint.append(Media(id(stream), media_type=format_conference_stream_type(stream))) + user.append(endpoint) + self.conference_info_payload.users = users + return self.conference_info_payload.toxml() + + def handle_incoming_subscription(self, subscribe_request, data): + content = self.build_conference_info_payload() + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=subscribe_request) + subscribe_request.accept(Conference.content_type, content) + self.subscriptions.append(subscribe_request) + + def accept_proposal(self, session, streams): + if session in self.sessions_with_proposals: + session.accept_proposal(streams) + self.sessions_with_proposals.remove(session) + + @run_in_twisted_thread + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + def _NH_AudioStreamDidTimeout(self, notification): + stream = notification.sender + session = stream._session + log.msg('Audio stream for session %s timed out' % format_identity(session.remote_identity)) + if session.streams == [stream]: + session.end() + + def _NH_ChatStreamGotMessage(self, notification): + data = notification.data.message + session = notification.sender.session + self.incoming_message_queue.send((session, 'message', data)) + + def _NH_ChatStreamGotComposingIndication(self, notification): + data = notification.data + session = notification.sender.session + self.incoming_message_queue.send((session, 'composing_indication', data)) + + def _NH_SIPIncomingSubscriptionDidEnd(self, notification): + subscription = notification.sender + notification_center = NotificationCenter() + notification_center.remove_observer(self, sender=subscription) + self.subscriptions.remove(subscription) + + def _NH_SIPSessionDidChangeHoldState(self, notification): + session = notification.sender + if notification.data.originator == 'remote': + if notification.data.on_hold: + log.msg('%s has put the audio session on hold' % format_identity(session.remote_identity)) + else: + log.msg('%s has taken the audio session out of hold' % format_identity(session.remote_identity)) + self.dispatch_conference_info() + + def _NH_SIPSessionGotProposal(self, notification): + session = notification.sender + audio_streams = [stream for stream in notification.data.streams if stream.type=='audio'] + chat_streams = [stream for stream in notification.data.streams if stream.type=='chat'] + if not audio_streams and not chat_streams: + session.reject_proposal() + return + streams = [streams[0] for streams in (audio_streams, chat_streams) if streams] + self.sessions_with_proposals.append(session) + reactor.callLater(4, self.accept_proposal, session, streams) + + def _NH_SIPSessionGotRejectProposal(self, notification): + session = notification.sender + self.sessions_with_proposals.remove(session) + + def _NH_SIPSessionDidRenegotiateStreams(self, notification): + notification_center = NotificationCenter() + session = notification.sender + streams = notification.data.streams + if notification.data.action == 'add': + try: + chat_stream = (stream for stream in streams if stream.type == 'chat').next() + except StopIteration: + pass + else: + notification_center.add_observer(self, sender=chat_stream) + remote_identity = CPIMIdentity.parse(format_identity(session.remote_identity, cpim_format=True)) + # getting last messages may take time, so new messages can arrive before messages the last message from history + for msg in database.get_last_messages(self.uri, ConferenceConfig.replay_history): + recipient = CPIMIdentity.parse(msg.cpim_recipient) + sender = CPIMIdentity.parse(msg.cpim_sender) + if recipient.uri in (self.identity.uri, remote_identity.uri) or sender.uri == remote_identity.uri: + chat_stream.send_message(msg.cpim_body, msg.cpim_content_type, local_identity=sender, recipients=[recipient], timestamp=msg.cpim_timestamp) + log.msg('%s has added chat to %s' % (format_identity(session.remote_identity), self.uri)) + self.dispatch_server_message('%s has added chat' % format_identity(session.remote_identity), exclude=session) + try: + audio_stream = (stream for stream in streams if stream.type == 'audio').next() + except StopIteration: + pass + else: + notification_center.add_observer(self, sender=audio_stream) + log.msg('Audio stream using %s/%sHz (%s), end-points: %s:%d <-> %s:%d' % (audio_stream.codec, audio_stream.sample_rate, + 'encrypted' if audio_stream.srtp_active else 'unencrypted', + audio_stream.local_rtp_address, audio_stream.local_rtp_port, + audio_stream.remote_rtp_address, audio_stream.remote_rtp_port)) + log.msg('%s has added audio to %s' % (format_identity(session.remote_identity), self.uri)) + self.dispatch_server_message('%s has added audio' % format_identity(session.remote_identity), exclude=session) + self.play_audio_welcome(session, False) + elif notification.data.action == 'remove': + try: + chat_stream = (stream for stream in streams if stream.type == 'chat').next() + except StopIteration: + pass + else: + notification_center.remove_observer(self, sender=chat_stream) + log.msg('%s has removed chat from %s' % (format_identity(session.remote_identity), self.uri)) + self.dispatch_server_message('%s has removed chat' % format_identity(session.remote_identity), exclude=session) + try: + audio_stream = (stream for stream in streams if stream.type == 'audio').next() + except StopIteration: + pass + else: + notification_center.remove_observer(self, sender=audio_stream) + try: + self.audio_conference.remove(audio_stream) + except ValueError: + # User may hangup before getting bridged into the conference + pass + if len(self.audio_conference.streams) == 0: + self.moh_player.pause() + self.audio_conference.hold() + elif len(self.audio_conference.streams) == 1: + self.moh_player.play() + log.msg('%s has removed audio from %s' % (format_identity(session.remote_identity), self.uri)) + self.dispatch_server_message('%s has removed audio' % format_identity(session.remote_identity), exclude=session) + if not session.streams: + log.msg('%s has removed all streams from %s, session will be terminated' % (format_identity(session.remote_identity), self.uri)) + session.end() + self.dispatch_conference_info() + + +class MoHPlayer(object): + implements(IObserver) + + def __init__(self, conference): + self.conference = conference + self.disabled = False + self.files = None + self.paused = False + self._player = None + + def initialize(self): + files = glob('%s/*.wav' % ResourcePath('sounds/moh').normalized) + if not files: + log.error('No files found, MoH is disabled') + self.disabled = True + return + random.shuffle(files) + self.files = cycle(files) + self._player = WavePlayer(SIPApplication.voice_audio_mixer, '', pause_time=1, initial_play=False, volume=20) + self.conference.bridge.add(self._player) + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=self._player) + + def stop(self): + if self.disabled: + return + notification_center = NotificationCenter() + notification_center.remove_observer(self, sender=self._player) + self.conference.bridge.remove(self, self._player) + self._player.stop() + self._player = None + + def play(self): + if not self.disabled: + self.paused = False + self._play_next_file() + log.msg('Started playing music on hold') + + def pause(self): + if not self.disabled: + self.paused = True + self._player.stop() + log.msg('Stopped playing music on hold') + + def _play_next_file(self): + file = self.files.next() + self._player.filename = str(file) + self._player.play() + + @run_in_twisted_thread + def handle_notification(self, notification): + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + def _NH_WavePlayerDidFail(self, notification): + if not self.paused: + self._play_next_file() + + def _NH_WavePlayerDidEnd(self, notification): + if not self.paused: + self._play_next_file() + diff --git a/sylk/configuration/__init__.py b/sylk/configuration/__init__.py new file mode 100644 index 0000000..b9f53ec --- /dev/null +++ b/sylk/configuration/__init__.py @@ -0,0 +1,52 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +from application.configuration import ConfigSection, ConfigSetting +from application.system import host +from sipsimple.configuration.datatypes import NonNegativeInteger, SRTPEncryption + +from sylk import configuration_filename +from sylk.configuration.datatypes import AudioCodecs, IPAddress, Port, PortRange + + +class ServerConfig(ConfigSection): + __cfgfile__ = configuration_filename + __section__ = 'Server' + + ca_file = ConfigSetting(type=str, value='/etc/sylkserver/tls/ca.crt') + certificate = ConfigSetting(type=str, value='/etc/sylkserver/tls/sylkserver.crt') + verify_server = False + default_application = 'conference' + trace_dir = ConfigSetting(type=str, value='/var/log/sylkserver') + trace_sip = False + trace_msrp = False + trace_notifications = False + + +class SIPConfig(ConfigSection): + __cfgfile__ = configuration_filename + __section__ = 'SIP' + + local_ip = ConfigSetting(type=IPAddress, value=host.default_ip) + local_udp_port = ConfigSetting(type=Port, value=5060) + local_tcp_port = ConfigSetting(type=Port, value=5060) + local_tls_port = ConfigSetting(type=Port, value=None) + + +class MSRPConfig(ConfigSection): + __cfgfile__ = configuration_filename + __section__ = 'MSRP' + + use_tls = False + + +class RTPConfig(ConfigSection): + __cfgfile__ = configuration_filename + __section__ = 'RTP' + + audio_codecs = ConfigSetting(type=AudioCodecs, value=None) + port_range = ConfigSetting(type=PortRange, value=PortRange('50000:50500')) + srtp_encryption = ConfigSetting(type=SRTPEncryption, value='optional') + timeout = ConfigSetting(type=NonNegativeInteger, value=30) + + diff --git a/sylk/configuration/backend.py b/sylk/configuration/backend.py new file mode 100644 index 0000000..d04d709 --- /dev/null +++ b/sylk/configuration/backend.py @@ -0,0 +1,26 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +__all__ = ['MemoryBackend'] + +from sipsimple.configuration.backend import IConfigurationBackend +from zope.interface import implements + + +class MemoryBackend(object): + """ + Implementation of a configuration backend that stores data in + memory. + """ + + implements(IConfigurationBackend) + + def __init__(self): + self._data = {} + + def load(self): + return self._data + + def save(self, data): + self._data = data + diff --git a/sylk/configuration/datatypes.py b/sylk/configuration/datatypes.py new file mode 100644 index 0000000..481354d --- /dev/null +++ b/sylk/configuration/datatypes.py @@ -0,0 +1,103 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +import os +import re +import socket +import sys + +from sipsimple.configuration.datatypes import AudioCodecList +from sipsimple.util import classproperty + + +class AudioCodecs(list): + def __new__(cls, value): + if isinstance(value, (tuple, list)): + return [str(x) for x in value if x in AudioCodecList.available_values] or None + elif isinstance(value, basestring): + if value.lower() in ('none', ''): + return None + return [x for x in re.split(r'\s*,\s*', value) if x in AudioCodecList.available_values] or None + else: + raise TypeError("value must be a string, list or tuple") + +class IPAddress(str): + """An IP address in quad dotted number notation""" + def __new__(cls, value): + if value == '0.0.0.0': + raise ValueError("%s is not allowed, please specify a specific IP address" % value) + else: + try: + socket.inet_aton(value) + except socket.error: + raise ValueError("invalid IP address: %r" % value) + except TypeError: + raise TypeError("value must be a string") + return str(value) + +class ResourcePath(object): + def __init__(self, path): + self.path = os.path.normpath(str(path)) + + def __getstate__(self): + return unicode(self.path) + + def __setstate__(self, state): + self.__init__(state) + + @property + def normalized(self): + path = os.path.expanduser(self.path) + if os.path.isabs(path): + return os.path.realpath(path) + return os.path.realpath(os.path.join(self.resources_directory, path)) + + @classproperty + def resources_directory(cls): + binary_directory = os.path.dirname(os.path.realpath(sys.argv[0])) + if os.path.basename(binary_directory) == 'bin': + application_directory = os.path.dirname(binary_directory) + else: + application_directory = binary_directory + from sipsimple.configuration.settings import SIPSimpleSettings + settings = SIPSimpleSettings() + if os.path.basename(binary_directory) == 'bin': + resources_component = settings.resources_directory or 'share/sylkserver' + else: + resources_component = settings.resources_directory or 'resources' + return os.path.realpath(os.path.join(application_directory, resources_component)) + + def __eq__(self, other): + try: + return self.path == other.path + except AttributeError: + return False + + def __hash__(self): + return hash(self.path) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.path) + + def __unicode__(self): + return unicode(self.path) + +class Port(int): + def __new__(cls, value): + try: + value = int(value) + except ValueError: + return None + if not (0 <= value <= 65535): + raise ValueError("illegal port value: %s" % value) + return value + +class PortRange(object): + """A port range in the form start:end with start and end being even numbers in the [1024, 65536] range""" + def __init__(self, value): + self.start, self.end = [int(p) for p in value.split(':', 1)] + allowed = xrange(1024, 65537, 2) + if not (self.start in allowed and self.end in allowed and self.start < self.end): + raise ValueError("bad range: %r: ports must be even numbers in the range [1024, 65536] with start < end" % value) + + diff --git a/sylk/configuration/settings.py b/sylk/configuration/settings.py new file mode 100644 index 0000000..f9090a4 --- /dev/null +++ b/sylk/configuration/settings.py @@ -0,0 +1,121 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +""" +SIP SIMPLE SDK settings extensions. +""" + +__all__ = ['AccountExtension', 'BonjourAccountExtension', 'SylkServerSettingsExtension'] + +from sipsimple.account import MSRPSettings as AccountMSRPSettings, NATTraversalSettings as AccountNATTraversalSettings +from sipsimple.account import RTPSettings as AccountRTPSettings, SIPSettings as AccountSIPSettings, TLSSettings as AccountTLSSettings +from sipsimple.configuration import CorrelatedSetting, Setting, SettingsObjectExtension +from sipsimple.configuration.datatypes import AudioCodecList, MSRPTransport, NonNegativeInteger, Path, Port, PortRange, SampleRate, SIPTransportList, SRTPEncryption +from sipsimple.configuration.settings import AudioSettings, LogsSettings, RTPSettings, SIPSettings, TLSSettings + +from sylk import __version__ as server_version +from sylk.configuration import ServerConfig, SIPConfig, MSRPConfig, RTPConfig + + +# Account settings extensions + +msrp_transport = transport = 'tls' if MSRPConfig.use_tls else 'tcp' +class AccountMSRPSettingsExtension(AccountMSRPSettings): + transport = Setting(type=MSRPTransport, default=msrp_transport) + +class AccountNATTraversalSettingsExtension(AccountNATTraversalSettings): + use_msrp_relay_for_inbound = Setting(type=bool, default=False) + use_msrp_relay_for_outbound = Setting(type=bool, default=False) + + +class AccountRTPSettingsExtension(AccountRTPSettings): + audio_codec_list = Setting(type=AudioCodecList, default=RTPConfig.audio_codecs, nillable=True) + srtp_encryption = Setting(type=SRTPEncryption, default=RTPConfig.srtp_encryption) + use_srtp_without_tls = Setting(type=bool, default=True) + + +class AccountSIPSettingsExtension(AccountSIPSettings): + register = Setting(type=bool, default=False) + + +tls_certificate = Path(ServerConfig.certificate) if ServerConfig.certificate else None +class AccountTLSSettingsExtension(AccountTLSSettings): + certificate = Setting(type=Path, default=tls_certificate, nillable=True) + verify_server = Setting(type=bool, default=ServerConfig.verify_server) + +class AccountExtension(SettingsObjectExtension): + enabled = Setting(type=bool, default=True) + + msrp = AccountMSRPSettingsExtension + nat_traversal = AccountNATTraversalSettingsExtension + rtp = AccountRTPSettingsExtension + sip = AccountSIPSettingsExtension + tls = AccountTLSSettingsExtension + + +class BonjourAccountExtension(SettingsObjectExtension): + enabled = Setting(type=bool, default=False) + + + +# General settings extensions + +class AudioSettingsExtension(AudioSettings): + input_device = Setting(type=str, default=None, nillable=True) + output_device = Setting(type=str, default=None, nillable=True) + sample_rate = Setting(type=SampleRate, default=16000) + + +log_directory = Path(ServerConfig.trace_dir) if ServerConfig.trace_dir else None +class LogsSettingsExtension(LogsSettings): + directory = Setting(type=Path, default=log_directory) + trace_sip = Setting(type=bool, default=ServerConfig.trace_sip) + trace_msrp = Setting(type=bool, default=ServerConfig.trace_msrp) + trace_pjsip = Setting(type=bool, default=True) + trace_notifications = Setting(type=bool, default=ServerConfig.trace_notifications) + + +class RTPSettingsExtension(RTPSettings): + port_range = Setting(type=PortRange, default=PortRange(RTPConfig.port_range.start, RTPConfig.port_range.end)) + timeout = Setting(type=NonNegativeInteger, default=RTPConfig.timeout) + + +def sip_port_validator(port, sibling_port): + if port == sibling_port != 0: + raise ValueError("the TCP and TLS ports must be different") + +transport_list = [] +if SIPConfig.local_udp_port: + transport_list.append('udp') +if SIPConfig.local_tcp_port: + transport_list.append('tcp') +if SIPConfig.local_tls_port: + transport_list.append('tls') + +udp_port = SIPConfig.local_udp_port or 0 +tcp_port = SIPConfig.local_tcp_port or 0 +tls_port = SIPConfig.local_tls_port or 0 + +class SIPSettingsExtension(SIPSettings): + udp_port = Setting(type=Port, default=udp_port) + tcp_port = CorrelatedSetting(type=Port, sibling='tls_port', validator=sip_port_validator, default=tcp_port) + tls_port = CorrelatedSetting(type=Port, sibling='tcp_port', validator=sip_port_validator, default=tls_port) + transport_list = Setting(type=SIPTransportList, default=transport_list) + + +ca_list = Path(ServerConfig.ca_file) if ServerConfig.ca_file else None +class TLSSettingsExtension(TLSSettings): + ca_list = Setting(type=Path, default=ca_list, nillable=True) + + +class SylkServerSettingsExtension(SettingsObjectExtension): + resources_directory = Setting(type=Path, default=None, nillable=True) + user_agent = Setting(type=str, default='SylkServer-%s' % server_version) + + audio = AudioSettingsExtension + logs = LogsSettingsExtension + rtp = RTPSettingsExtension + sip = SIPSettingsExtension + tls = TLSSettingsExtension + + diff --git a/sylk/database.py b/sylk/database.py new file mode 100644 index 0000000..7769fad --- /dev/null +++ b/sylk/database.py @@ -0,0 +1,45 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +""" +Database connection factory + +Each application that wants to connect to a database should instantiate a Database +object with the URI it wants to connect to. As Database is a Singleton, same object +will be used if the same URI is specified. + +A usage example can be found in the conference application database module. +""" + +__all__ = ['Database'] + +from application import log +from application.python.util import Null, Singleton +from sqlobject import connectionForURI + + +class Database(object): + __metaclass__ = Singleton + + def __init__(self, uri): + if uri == 'sqlite:/:memory:': + log.warn("SQLite memory backend can't be used because it's not thread-safe") + uri = None + self.uri = uri + if uri is not None: + self.connection = connectionForURI(uri) + else: + self.connection = Null + + def create_table(self, klass): + if klass._connection is Null or klass.tableExists(): + return + else: + log.warn('Table %s does not exists. Creating it now.' % klass.sqlmeta.table) + saved = klass._connection.debug + try: + klass._connection.debug = True # log SQL used to create the table + klass.createTable() + finally: + klass._connection.debug = saved + diff --git a/sylk/extensions.py b/sylk/extensions.py new file mode 100644 index 0000000..455ee40 --- /dev/null +++ b/sylk/extensions.py @@ -0,0 +1,73 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +import random + +from datetime import datetime + +from msrplib.session import contains_mime_type +from sipsimple.account import AccountManager +from sipsimple.core import SDPAttribute +from sipsimple.payloads.iscomposing import IsComposingMessage, State, LastActive, Refresh, ContentType +from sipsimple.streams import MediaStreamRegistry +from sipsimple.streams.applications.chat import CPIMMessage +from sipsimple.streams.msrp import ChatStream as _ChatStream, ChatStreamError, MSRPStreamBase + + +# We need to match on the only account that will be available +def _always_find_default_account(self, contact_uri): + return self.default_account +AccountManager.find_account = _always_find_default_account + + +# We need to be able to set the local identity in the message CPIM envelope +# so that messages appear to be coming from the users themselves, instead of +# just seeying the server identity +registry = MediaStreamRegistry() +for stream_type in registry.stream_types[:]: + if stream_type is _ChatStream: + registry.stream_types.remove(stream_type) + break +del registry + +class ChatStream(_ChatStream): + accept_types = ['message/cpim'] + accept_wrapped_types = ['*'] + + def _create_local_media(self, uri_path): + local_media = MSRPStreamBase._create_local_media(self, uri_path) + if self.session.local_focus: + local_media.attributes.append(SDPAttribute('chatroom', 'private-messages')) + return local_media + + def send_message(self, content, content_type='text/plain', local_identity=None, recipients=None, courtesy_recipients=None, subject=None, timestamp=None, required=None, additional_headers=None): + if self.direction=='recvonly': + raise ChatStreamError('Cannot send message on recvonly stream') + message_id = '%x' % random.getrandbits(64) + if not contains_mime_type(self.accept_wrapped_types, content_type): + raise ChatStreamError('Invalid content_type for outgoing message: %r' % content_type) + if not recipients: + recipients = [self.remote_identity] + if timestamp is None: + timestamp = datetime.now() + # Only use CPIM, it's the only type we accept + msg = CPIMMessage(content, content_type, sender=local_identity or self.local_identity, recipients=recipients, courtesy_recipients=courtesy_recipients, + subject=subject, timestamp=timestamp, required=required, additional_headers=additional_headers) + self._enqueue_message(message_id, str(msg), 'message/cpim', failure_report='yes', success_report='yes', notify_progress=True) + return message_id + + def send_composing_indication(self, state, refresh, last_active=None, recipients=None, local_identity=None): + if self.direction == 'recvonly': + raise ChatStreamError('Cannot send message on recvonly stream') + if state not in ('active', 'idle'): + raise ValueError('Invalid value for composing indication state') + message_id = '%x' % random.getrandbits(64) + content = IsComposingMessage(state=State(state), refresh=Refresh(refresh), last_active=LastActive(last_active or datetime.now()), content_type=ContentType('text')).toxml() + if recipients is None: + recipients = [self.remote_identity] + # Only use CPIM, it's the only type we accept + msg = CPIMMessage(content, IsComposingMessage.content_type, sender=local_identity or self.local_identity, recipients=recipients, timestamp=datetime.now()) + self._enqueue_message(message_id, str(msg), 'message/cpim', failure_report='partial', success_report='no') + return message_id + + diff --git a/sylk/log.py b/sylk/log.py new file mode 100644 index 0000000..66401e6 --- /dev/null +++ b/sylk/log.py @@ -0,0 +1,307 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +""" +Logging support adapted from SIP SIMPLE Client logger. +""" + +from __future__ import with_statement + +__all__ = ["Logger"] + +import datetime +import os +import sys + +from pprint import pformat + +from application import log +from application.notification import IObserver, NotificationCenter +from application.python.queue import EventQueue +from application.python.util import Null +from sipsimple.configuration.settings import SIPSimpleSettings +from sipsimple.util import makedirs +from zope.interface import implements + + +class Logger(object): + implements(IObserver) + + # public methods + # + + def __init__(self, msrp_level=log.level.ERROR): + self.msrp_level = msrp_level + + self._siptrace_filename = None + self._siptrace_file = None + self._siptrace_error = False + self._siptrace_start_time = None + self._siptrace_packet_count = 0 + + self._msrptrace_filename = None + self._msrptrace_file = None + self._msrptrace_error = False + + self._pjsiptrace_filename = None + self._pjsiptrace_file = None + self._pjsiptrace_error = False + + self._notifications_filename = None + self._notifications_file = None + self._notifications_error = False + + self._event_queue = EventQueue(handler=self._process_notification, name='Log handling') + self._log_directory_error = False + + def start(self): + # try to create the log directory + try: + self._init_log_directory() + except Exception: + pass + + # register to receive log notifications + notification_center = NotificationCenter() + notification_center.add_observer(self) + + # start the thread processing the notifications + self._event_queue.start() + + def stop(self): + # stop the thread processing the notifications + self._event_queue.stop() + self._event_queue.join() + + # close sip trace file + if self._siptrace_file is not None: + self._siptrace_file.close() + self._siptrace_file = None + + # close msrp trace file + if self._msrptrace_file is not None: + self._msrptrace_file.close() + self._msrptrace_file = None + + # close pjsip trace file + if self._pjsiptrace_file is not None: + self._pjsiptrace_file.close() + self._pjsiptrace_file = None + + # close notifications trace file + if self._notifications_file is not None: + self._notifications_file.close() + self._notifications_file = None + + # unregister from receiving notifications + notification_center = NotificationCenter() + notification_center.remove_observer(self) + + def handle_notification(self, notification): + self._event_queue.put(notification) + + def _process_notification(self, notification): + settings = SIPSimpleSettings() + handler = getattr(self, '_NH_%s' % notification.name, Null) + handler(notification) + + handler = getattr(self, '_LH_%s' % notification.name, Null) + handler(notification) + + if notification.name not in ('SIPEngineLog', 'SIPEngineSIPTrace') and settings.logs.trace_notifications: + message = 'Notification name=%s sender=%s' % (notification.name, notification.sender) + if notification.data is not None: + message += '\n%s' % pformat(notification.data.__dict__) + if settings.logs.trace_notifications: + try: + self._init_log_file('notifications') + except Exception: + pass + else: + self._notifications_file.write('%s [%s %d]: %s\n' % (datetime.datetime.now(), os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) + self._notifications_file.flush() + + # notification handlers + # + + def _NH_CFGSettingsObjectDidChange(self, notification): + settings = SIPSimpleSettings() + if notification.sender is settings: + if 'logs.directory' in notification.data.modified: + # sip trace + if self._siptrace_file is not None: + self._siptrace_file.close() + self._siptrace_file = None + # pjsip trace + if self._pjsiptrace_file is not None: + self._pjsiptrace_file.close() + self._pjsiptrace_file = None + # notifications trace + if self._notifications_file is not None: + self._notifications_file.close() + self._notifications_file = None + # try to create the log directory + try: + self._init_log_directory() + except Exception: + pass + + # log handlers + # + + def _LH_SIPEngineSIPTrace(self, notification): + settings = SIPSimpleSettings() + if not settings.logs.trace_sip: + return + if self._siptrace_start_time is None: + self._siptrace_start_time = notification.data.timestamp + self._siptrace_packet_count += 1 + if notification.data.received: + direction = "RECEIVED" + else: + direction = "SENDING" + buf = ["%s: Packet %d, +%s" % (direction, self._siptrace_packet_count, (notification.data.timestamp - self._siptrace_start_time))] + buf.append("%(source_ip)s:%(source_port)d -(SIP over %(transport)s)-> %(destination_ip)s:%(destination_port)d" % notification.data.__dict__) + buf.append(notification.data.data) + buf.append('--') + message = '\n'.join(buf) + if settings.logs.trace_sip: + try: + self._init_log_file('siptrace') + except Exception: + pass + else: + self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) + self._siptrace_file.flush() + + def _LH_SIPEngineLog(self, notification): + settings = SIPSimpleSettings() + if not settings.logs.trace_pjsip: + return + message = "(%(level)d) %(sender)14s: %(message)s" % notification.data.__dict__ + if settings.logs.trace_pjsip: + try: + self._init_log_file('pjsiptrace') + except Exception: + pass + else: + self._pjsiptrace_file.write('%s [%s %d] %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) + self._pjsiptrace_file.flush() + + def _LH_DNSLookupTrace(self, notification): + settings = SIPSimpleSettings() + if not settings.logs.trace_sip: + return + message = 'DNS lookup %(query_type)s %(query_name)s' % notification.data.__dict__ + if notification.data.error is None: + message += ' succeeded, ttl=%d: ' % notification.data.answer.ttl + if notification.data.query_type == 'A': + message += ", ".join(record.address for record in notification.data.answer) + elif notification.data.query_type == 'SRV': + message += ", ".join('%d %d %d %s' % (record.priority, record.weight, record.port, record.target) for record in notification.data.answer) + elif notification.data.query_type == 'NAPTR': + message += ", ".join('%d %d "%s" "%s" "%s" %s' % (record.order, record.preference, record.flags, record.service, record.regexp, record.replacement) for record in notification.data.answer) + else: + import dns.resolver + message_map = {dns.resolver.NXDOMAIN: 'DNS record does not exist', + dns.resolver.NoAnswer: 'DNS response contains no answer', + dns.resolver.NoNameservers: 'no DNS name servers could be reached', + dns.resolver.Timeout: 'no DNS response received, the query has timed out'} + message += ' failed: %s' % message_map.get(notification.data.error.__class__, '') + if settings.logs.trace_sip: + try: + self._init_log_file('siptrace') + except Exception: + pass + else: + self._siptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) + self._siptrace_file.flush() + + def _LH_MSRPTransportTrace(self, notification): + settings = SIPSimpleSettings() + if not settings.logs.trace_msrp: + return + arrow = {'incoming': '<--', 'outgoing': '-->'}[notification.data.direction] + local_address = notification.sender.getHost() + local_address = '%s:%d' % (local_address.host, local_address.port) + remote_address = notification.sender.getPeer() + remote_address = '%s:%d' % (remote_address.host, remote_address.port) + message = '%s %s %s\n' % (local_address, arrow, remote_address) + notification.data.data + if settings.logs.trace_msrp: + try: + self._init_log_file('msrptrace') + except Exception: + pass + else: + self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) + self._msrptrace_file.flush() + + def _LH_MSRPLibraryLog(self, notification): + settings = SIPSimpleSettings() + if not settings.logs.trace_msrp: + return + if notification.data.level < self.msrp_level: + return + message = '%s%s' % (notification.data.level.prefix, notification.data.message) + if settings.logs.trace_msrp: + try: + self._init_log_file('msrptrace') + except Exception: + pass + else: + self._msrptrace_file.write('%s [%s %d]: %s\n' % (notification.data.timestamp, os.path.basename(sys.argv[0]).rstrip('.py'), os.getpid(), message)) + self._msrptrace_file.flush() + + # private methods + # + + def _init_log_directory(self): + settings = SIPSimpleSettings() + log_directory = settings.logs.directory.normalized + try: + makedirs(log_directory) + except Exception, e: + if not self._log_directory_error: + print "failed to create logs directory '%s': %s" % (log_directory, e) + self._log_directory_error = True + self._siptrace_error = True + self._pjsiptrace_error = True + self._notifications_error = True + raise + else: + self._log_directory_error = False + # sip trace + if self._siptrace_filename is None: + self._siptrace_filename = os.path.join(log_directory, 'sip_trace.txt') + self._siptrace_error = False + + # msrp trace + if self._msrptrace_filename is None: + self._msrptrace_filename = os.path.join(log_directory, 'msrp_trace.txt') + self._msrptrace_error = False + + # pjsip trace + if self._pjsiptrace_filename is None: + self._pjsiptrace_filename = os.path.join(log_directory, 'pjsip_trace.txt') + self._pjsiptrace_error = False + + # notifications trace + if self._notifications_filename is None: + self._notifications_filename = os.path.join(log_directory, 'notifications_trace.txt') + self._notifications_error = False + + def _init_log_file(self, type): + if getattr(self, '_%s_file' % type) is None: + self._init_log_directory() + filename = getattr(self, '_%s_filename' % type) + try: + setattr(self, '_%s_file' % type, open(filename, 'a')) + except Exception, e: + if not getattr(self, '_%s_error' % type): + print "failed to create log file '%s': %s" % (filename, e) + setattr(self, '_%s_error' % type, True) + raise + else: + setattr(self, '_%s_error' % type, False) + + diff --git a/sylk/server.py b/sylk/server.py new file mode 100644 index 0000000..fb62d93 --- /dev/null +++ b/sylk/server.py @@ -0,0 +1,172 @@ +# Copyright (C) 2010-2011 AG Projects. See LICENSE for details. +# + +import sys + +from threading import Event + +from application import log +from application.notification import NotificationCenter +from sipsimple.account import Account, BonjourAccount, AccountManager +from sipsimple.application import SIPApplication +from sipsimple.audio import AudioDevice, RootAudioBridge +from sipsimple.configuration import ConfigurationError +from sipsimple.configuration.settings import SIPSimpleSettings +from sipsimple.core import AudioMixer, Engine, SIPCoreError +from sipsimple.session import SessionManager +from sipsimple.util import TimestampedNotificationData +from twisted.internet import reactor + +from sylk.applications import IncomingRequestHandler +from sylk.configuration import SIPConfig +from sylk.configuration.backend import MemoryBackend +from sylk.configuration.settings import AccountExtension, BonjourAccountExtension, SylkServerSettingsExtension +from sylk.log import Logger + +# Load extensions needed for integration with SIP SIMPLE SDK +import sylk.extensions + + +class SylkServer(SIPApplication): + + def __init__(self): + self.logger = None + self.request_handler = IncomingRequestHandler() + self.stop_event = Event() + + def start(self): + notification_center = NotificationCenter() + notification_center.add_observer(self, sender=self) + + self.logger = Logger() + + Account.register_extension(AccountExtension) + BonjourAccount.register_extension(BonjourAccountExtension) + SIPSimpleSettings.register_extension(SylkServerSettingsExtension) + + try: + SIPApplication.start(self, MemoryBackend()) + except ConfigurationError, e: + log.fatal("Error loading configuration: ",e) + sys.exit(1) + + def _load_configuration(self): + account_manager = AccountManager() + account = Account("account@example.com") # an account is required by AccountManager + account_manager.default_account = account + + def _initialize_subsystems(self): + account_manager = AccountManager() + engine = Engine() + notification_center = NotificationCenter() + session_manager = SessionManager() + settings = SIPSimpleSettings() + self._load_configuration() + + notification_center.post_notification('SIPApplicationWillStart', sender=self, data=TimestampedNotificationData()) + if self.state == 'stopping': + reactor.stop() + return + + account = account_manager.default_account + + # initialize core + notification_center.add_observer(self, sender=engine) + options = dict(# general + ip_address=SIPConfig.local_ip, + user_agent=settings.user_agent, + # SIP + udp_port=settings.sip.udp_port if 'udp' in settings.sip.transport_list else None, + tcp_port=settings.sip.tcp_port if 'tcp' in settings.sip.transport_list else None, + tls_port=None, + # TLS + tls_protocol='TLSv1', + tls_verify_server=False, + tls_ca_file=None, + tls_cert_file=None, + tls_privkey_file=None, + tls_timeout=3000, + # rtp + rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end), + # audio + codecs=list(settings.rtp.audio_codec_list), + # logging + log_level=settings.logs.pjsip_level, + trace_sip=True, + # events and requests to handle + events={"conference": ["application/conference-info+xml"]}, + incoming_events=set(['conference']), + incoming_requests=set(['MESSAGE']) + ) + try: + engine.start(**options) + except SIPCoreError: + self.end_reason = 'engine failed' + reactor.stop() + return + + # initialize TLS + try: + engine.set_tls_options(port=settings.sip.tls_port if 'tls' in settings.sip.transport_list else None, + protocol=settings.tls.protocol, + verify_server=account.tls.verify_server if account else False, + ca_file=settings.tls.ca_list.normalized if settings.tls.ca_list else None, + cert_file=account.tls.certificate.normalized if account and account.tls.certificate else None, + privkey_file=account.tls.certificate.normalized if account and account.tls.certificate else None, + timeout=settings.tls.timeout) + except Exception, e: + notification_center.post_notification('SIPApplicationFailedToStartTLS', sender=self, data=TimestampedNotificationData(error=e)) + + # initialize audio objects + voice_mixer = AudioMixer(None, None, settings.audio.sample_rate, settings.audio.tail_length) + self.voice_audio_device = AudioDevice(voice_mixer) + self.voice_audio_bridge = RootAudioBridge(voice_mixer) + self.voice_audio_bridge.add(self.voice_audio_device) + + # initialize middleware components + account_manager.start() + session_manager.start() + + notification_center.add_observer(self, name='CFGSettingsObjectDidChange') + + self.state = 'started' + notification_center.post_notification('SIPApplicationDidStart', sender=self, data=TimestampedNotificationData()) + + def _NH_SIPApplicationFailedToStartTLS(self, notification): + log.fatal("Couldn't set TLS options: %s" % notification.data.error) + + def _NH_SIPApplicationWillStart(self, notification): + self.logger.start() + self.request_handler.start() + settings = SIPSimpleSettings() + if settings.logs.trace_sip and self.logger._siptrace_filename is not None: + log.msg('Logging SIP trace to file "%s"' % self.logger._siptrace_filename) + if settings.logs.trace_msrp and self.logger._msrptrace_filename is not None: + log.msg('Logging MSRP trace to file "%s"' % self.logger._msrptrace_filename) + if settings.logs.trace_pjsip and self.logger._pjsiptrace_filename is not None: + log.msg('Logging PJSIP trace to file "%s"' % self.logger._pjsiptrace_filename) + if settings.logs.trace_notifications and self.logger._notifications_filename is not None: + log.msg('Logging notifications trace to file "%s"' % self.logger._notifications_filename) + + def _NH_SIPApplicationDidStart(self, notification): + engine = Engine() + settings = SIPSimpleSettings() + local_ip = SIPConfig.local_ip + log.msg("SylkServer started, listening on:") + for transport in settings.sip.transport_list: + try: + log.msg("%s:%d (%s)" % (local_ip, getattr(engine, '%s_port' % transport), transport.upper())) + except TypeError: + pass + + def _NH_SIPApplicationWillEnd(self, notification): + self.request_handler.stop() + + def _NH_SIPApplicationDidEnd(self, notification): + self.logger.stop() + self.stop_event.set() + + def _NH_SIPEngineGotException(self, notification): + log.error('An exception occured within the SIP core:\n%s\n' % notification.data.traceback) + +