Changeset 545 for branches/stable
- Timestamp:
- 12/10/13 17:47:44 (11 years ago)
- Location:
- branches/stable
- Files:
-
- 25 edited
- 6 copied
Legend:
- Unmodified
- Added
- Removed
-
branches/stable
- Property svn:mergeinfo changed
/trunk merged: 264-316,318-416
- Property svn:mergeinfo changed
-
branches/stable/COPYING
r114 r545 673 673 Public License instead of this License. But first, please read 674 674 <http://www.gnu.org/philosophy/why-not-lgpl.html>. 675 GNU GENERAL PUBLIC LICENSE676 Version 3, 29 June 2007677 678 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>679 Everyone is permitted to copy and distribute verbatim copies680 of this license document, but changing it is not allowed.681 682 Preamble683 684 The GNU General Public License is a free, copyleft license for685 software and other kinds of works.686 687 The licenses for most software and other practical works are designed688 to take away your freedom to share and change the works. By contrast,689 the GNU General Public License is intended to guarantee your freedom to690 share and change all versions of a program--to make sure it remains free691 software for all its users. We, the Free Software Foundation, use the692 GNU General Public License for most of our software; it applies also to693 any other work released this way by its authors. You can apply it to694 your programs, too.695 696 When we speak of free software, we are referring to freedom, not697 price. Our General Public Licenses are designed to make sure that you698 have the freedom to distribute copies of free software (and charge for699 them if you wish), that you receive source code or can get it if you700 want it, that you can change the software or use pieces of it in new701 free programs, and that you know you can do these things.702 703 To protect your rights, we need to prevent others from denying you704 these rights or asking you to surrender the rights. Therefore, you have705 certain responsibilities if you distribute copies of the software, or if706 you modify it: responsibilities to respect the freedom of others.707 708 For example, if you distribute copies of such a program, whether709 gratis or for a fee, you must pass on to the recipients the same710 freedoms that you received. You must make sure that they, too, receive711 or can get the source code. And you must show them these terms so they712 know their rights.713 714 Developers that use the GNU GPL protect your rights with two steps:715 (1) assert copyright on the software, and (2) offer you this License716 giving you legal permission to copy, distribute and/or modify it.717 718 For the developers' and authors' protection, the GPL clearly explains719 that there is no warranty for this free software. For both users' and720 authors' sake, the GPL requires that modified versions be marked as721 changed, so that their problems will not be attributed erroneously to722 authors of previous versions.723 724 Some devices are designed to deny users access to install or run725 modified versions of the software inside them, although the manufacturer726 can do so. This is fundamentally incompatible with the aim of727 protecting users' freedom to change the software. The systematic728 pattern of such abuse occurs in the area of products for individuals to729 use, which is precisely where it is most unacceptable. Therefore, we730 have designed this version of the GPL to prohibit the practice for those731 products. If such problems arise substantially in other domains, we732 stand ready to extend this provision to those domains in future versions733 of the GPL, as needed to protect the freedom of users.734 735 Finally, every program is threatened constantly by software patents.736 States should not allow patents to restrict development and use of737 software on general-purpose computers, but in those that do, we wish to738 avoid the special danger that patents applied to a free program could739 make it effectively proprietary. To prevent this, the GPL assures that740 patents cannot be used to render the program non-free.741 742 The precise terms and conditions for copying, distribution and743 modification follow.744 745 TERMS AND CONDITIONS746 747 0. Definitions.748 749 "This License" refers to version 3 of the GNU General Public License.750 751 "Copyright" also means copyright-like laws that apply to other kinds of752 works, such as semiconductor masks.753 754 "The Program" refers to any copyrightable work licensed under this755 License. Each licensee is addressed as "you". "Licensees" and756 "recipients" may be individuals or organizations.757 758 To "modify" a work means to copy from or adapt all or part of the work759 in a fashion requiring copyright permission, other than the making of an760 exact copy. The resulting work is called a "modified version" of the761 earlier work or a work "based on" the earlier work.762 763 A "covered work" means either the unmodified Program or a work based764 on the Program.765 766 To "propagate" a work means to do anything with it that, without767 permission, would make you directly or secondarily liable for768 infringement under applicable copyright law, except executing it on a769 computer or modifying a private copy. Propagation includes copying,770 distribution (with or without modification), making available to the771 public, and in some countries other activities as well.772 773 To "convey" a work means any kind of propagation that enables other774 parties to make or receive copies. Mere interaction with a user through775 a computer network, with no transfer of a copy, is not conveying.776 777 An interactive user interface displays "Appropriate Legal Notices"778 to the extent that it includes a convenient and prominently visible779 feature that (1) displays an appropriate copyright notice, and (2)780 tells the user that there is no warranty for the work (except to the781 extent that warranties are provided), that licensees may convey the782 work under this License, and how to view a copy of this License. If783 the interface presents a list of user commands or options, such as a784 menu, a prominent item in the list meets this criterion.785 786 1. Source Code.787 788 The "source code" for a work means the preferred form of the work789 for making modifications to it. "Object code" means any non-source790 form of a work.791 792 A "Standard Interface" means an interface that either is an official793 standard defined by a recognized standards body, or, in the case of794 interfaces specified for a particular programming language, one that795 is widely used among developers working in that language.796 797 The "System Libraries" of an executable work include anything, other798 than the work as a whole, that (a) is included in the normal form of799 packaging a Major Component, but which is not part of that Major800 Component, and (b) serves only to enable use of the work with that801 Major Component, or to implement a Standard Interface for which an802 implementation is available to the public in source code form. A803 "Major Component", in this context, means a major essential component804 (kernel, window system, and so on) of the specific operating system805 (if any) on which the executable work runs, or a compiler used to806 produce the work, or an object code interpreter used to run it.807 808 The "Corresponding Source" for a work in object code form means all809 the source code needed to generate, install, and (for an executable810 work) run the object code and to modify the work, including scripts to811 control those activities. However, it does not include the work's812 System Libraries, or general-purpose tools or generally available free813 programs which are used unmodified in performing those activities but814 which are not part of the work. For example, Corresponding Source815 includes interface definition files associated with source files for816 the work, and the source code for shared libraries and dynamically817 linked subprograms that the work is specifically designed to require,818 such as by intimate data communication or control flow between those819 subprograms and other parts of the work.820 821 The Corresponding Source need not include anything that users822 can regenerate automatically from other parts of the Corresponding823 Source.824 825 The Corresponding Source for a work in source code form is that826 same work.827 828 2. Basic Permissions.829 830 All rights granted under this License are granted for the term of831 copyright on the Program, and are irrevocable provided the stated832 conditions are met. This License explicitly affirms your unlimited833 permission to run the unmodified Program. The output from running a834 covered work is covered by this License only if the output, given its835 content, constitutes a covered work. This License acknowledges your836 rights of fair use or other equivalent, as provided by copyright law.837 838 You may make, run and propagate covered works that you do not839 convey, without conditions so long as your license otherwise remains840 in force. You may convey covered works to others for the sole purpose841 of having them make modifications exclusively for you, or provide you842 with facilities for running those works, provided that you comply with843 the terms of this License in conveying all material for which you do844 not control copyright. Those thus making or running the covered works845 for you must do so exclusively on your behalf, under your direction846 and control, on terms that prohibit them from making any copies of847 your copyrighted material outside their relationship with you.848 849 Conveying under any other circumstances is permitted solely under850 the conditions stated below. Sublicensing is not allowed; section 10851 makes it unnecessary.852 853 3. Protecting Users' Legal Rights From Anti-Circumvention Law.854 855 No covered work shall be deemed part of an effective technological856 measure under any applicable law fulfilling obligations under article857 11 of the WIPO copyright treaty adopted on 20 December 1996, or858 similar laws prohibiting or restricting circumvention of such859 measures.860 861 When you convey a covered work, you waive any legal power to forbid862 circumvention of technological measures to the extent such circumvention863 is effected by exercising rights under this License with respect to864 the covered work, and you disclaim any intention to limit operation or865 modification of the work as a means of enforcing, against the work's866 users, your or third parties' legal rights to forbid circumvention of867 technological measures.868 869 4. Conveying Verbatim Copies.870 871 You may convey verbatim copies of the Program's source code as you872 receive it, in any medium, provided that you conspicuously and873 appropriately publish on each copy an appropriate copyright notice;874 keep intact all notices stating that this License and any875 non-permissive terms added in accord with section 7 apply to the code;876 keep intact all notices of the absence of any warranty; and give all877 recipients a copy of this License along with the Program.878 879 You may charge any price or no price for each copy that you convey,880 and you may offer support or warranty protection for a fee.881 882 5. Conveying Modified Source Versions.883 884 You may convey a work based on the Program, or the modifications to885 produce it from the Program, in the form of source code under the886 terms of section 4, provided that you also meet all of these conditions:887 888 a) The work must carry prominent notices stating that you modified889 it, and giving a relevant date.890 891 b) The work must carry prominent notices stating that it is892 released under this License and any conditions added under section893 7. This requirement modifies the requirement in section 4 to894 "keep intact all notices".895 896 c) You must license the entire work, as a whole, under this897 License to anyone who comes into possession of a copy. This898 License will therefore apply, along with any applicable section 7899 additional terms, to the whole of the work, and all its parts,900 regardless of how they are packaged. This License gives no901 permission to license the work in any other way, but it does not902 invalidate such permission if you have separately received it.903 904 d) If the work has interactive user interfaces, each must display905 Appropriate Legal Notices; however, if the Program has interactive906 interfaces that do not display Appropriate Legal Notices, your907 work need not make them do so.908 909 A compilation of a covered work with other separate and independent910 works, which are not by their nature extensions of the covered work,911 and which are not combined with it such as to form a larger program,912 in or on a volume of a storage or distribution medium, is called an913 "aggregate" if the compilation and its resulting copyright are not914 used to limit the access or legal rights of the compilation's users915 beyond what the individual works permit. Inclusion of a covered work916 in an aggregate does not cause this License to apply to the other917 parts of the aggregate.918 919 6. Conveying Non-Source Forms.920 921 You may convey a covered work in object code form under the terms922 of sections 4 and 5, provided that you also convey the923 machine-readable Corresponding Source under the terms of this License,924 in one of these ways:925 926 a) Convey the object code in, or embodied in, a physical product927 (including a physical distribution medium), accompanied by the928 Corresponding Source fixed on a durable physical medium929 customarily used for software interchange.930 931 b) Convey the object code in, or embodied in, a physical product932 (including a physical distribution medium), accompanied by a933 written offer, valid for at least three years and valid for as934 long as you offer spare parts or customer support for that product935 model, to give anyone who possesses the object code either (1) a936 copy of the Corresponding Source for all the software in the937 product that is covered by this License, on a durable physical938 medium customarily used for software interchange, for a price no939 more than your reasonable cost of physically performing this940 conveying of source, or (2) access to copy the941 Corresponding Source from a network server at no charge.942 943 c) Convey individual copies of the object code with a copy of the944 written offer to provide the Corresponding Source. This945 alternative is allowed only occasionally and noncommercially, and946 only if you received the object code with such an offer, in accord947 with subsection 6b.948 949 d) Convey the object code by offering access from a designated950 place (gratis or for a charge), and offer equivalent access to the951 Corresponding Source in the same way through the same place at no952 further charge. You need not require recipients to copy the953 Corresponding Source along with the object code. If the place to954 copy the object code is a network server, the Corresponding Source955 may be on a different server (operated by you or a third party)956 that supports equivalent copying facilities, provided you maintain957 clear directions next to the object code saying where to find the958 Corresponding Source. Regardless of what server hosts the959 Corresponding Source, you remain obligated to ensure that it is960 available for as long as needed to satisfy these requirements.961 962 e) Convey the object code using peer-to-peer transmission, provided963 you inform other peers where the object code and Corresponding964 Source of the work are being offered to the general public at no965 charge under subsection 6d.966 967 A separable portion of the object code, whose source code is excluded968 from the Corresponding Source as a System Library, need not be969 included in conveying the object code work.970 971 A "User Product" is either (1) a "consumer product", which means any972 tangible personal property which is normally used for personal, family,973 or household purposes, or (2) anything designed or sold for incorporation974 into a dwelling. In determining whether a product is a consumer product,975 doubtful cases shall be resolved in favor of coverage. For a particular976 product received by a particular user, "normally used" refers to a977 typical or common use of that class of product, regardless of the status978 of the particular user or of the way in which the particular user979 actually uses, or expects or is expected to use, the product. A product980 is a consumer product regardless of whether the product has substantial981 commercial, industrial or non-consumer uses, unless such uses represent982 the only significant mode of use of the product.983 984 "Installation Information" for a User Product means any methods,985 procedures, authorization keys, or other information required to install986 and execute modified versions of a covered work in that User Product from987 a modified version of its Corresponding Source. The information must988 suffice to ensure that the continued functioning of the modified object989 code is in no case prevented or interfered with solely because990 modification has been made.991 992 If you convey an object code work under this section in, or with, or993 specifically for use in, a User Product, and the conveying occurs as994 part of a transaction in which the right of possession and use of the995 User Product is transferred to the recipient in perpetuity or for a996 fixed term (regardless of how the transaction is characterized), the997 Corresponding Source conveyed under this section must be accompanied998 by the Installation Information. But this requirement does not apply999 if neither you nor any third party retains the ability to install1000 modified object code on the User Product (for example, the work has1001 been installed in ROM).1002 1003 The requirement to provide Installation Information does not include a1004 requirement to continue to provide support service, warranty, or updates1005 for a work that has been modified or installed by the recipient, or for1006 the User Product in which it has been modified or installed. Access to a1007 network may be denied when the modification itself materially and1008 adversely affects the operation of the network or violates the rules and1009 protocols for communication across the network.1010 1011 Corresponding Source conveyed, and Installation Information provided,1012 in accord with this section must be in a format that is publicly1013 documented (and with an implementation available to the public in1014 source code form), and must require no special password or key for1015 unpacking, reading or copying.1016 1017 7. Additional Terms.1018 1019 "Additional permissions" are terms that supplement the terms of this1020 License by making exceptions from one or more of its conditions.1021 Additional permissions that are applicable to the entire Program shall1022 be treated as though they were included in this License, to the extent1023 that they are valid under applicable law. If additional permissions1024 apply only to part of the Program, that part may be used separately1025 under those permissions, but the entire Program remains governed by1026 this License without regard to the additional permissions.1027 1028 When you convey a copy of a covered work, you may at your option1029 remove any additional permissions from that copy, or from any part of1030 it. (Additional permissions may be written to require their own1031 removal in certain cases when you modify the work.) You may place1032 additional permissions on material, added by you to a covered work,1033 for which you have or can give appropriate copyright permission.1034 1035 Notwithstanding any other provision of this License, for material you1036 add to a covered work, you may (if authorized by the copyright holders of1037 that material) supplement the terms of this License with terms:1038 1039 a) Disclaiming warranty or limiting liability differently from the1040 terms of sections 15 and 16 of this License; or1041 1042 b) Requiring preservation of specified reasonable legal notices or1043 author attributions in that material or in the Appropriate Legal1044 Notices displayed by works containing it; or1045 1046 c) Prohibiting misrepresentation of the origin of that material, or1047 requiring that modified versions of such material be marked in1048 reasonable ways as different from the original version; or1049 1050 d) Limiting the use for publicity purposes of names of licensors or1051 authors of the material; or1052 1053 e) Declining to grant rights under trademark law for use of some1054 trade names, trademarks, or service marks; or1055 1056 f) Requiring indemnification of licensors and authors of that1057 material by anyone who conveys the material (or modified versions of1058 it) with contractual assumptions of liability to the recipient, for1059 any liability that these contractual assumptions directly impose on1060 those licensors and authors.1061 1062 All other non-permissive additional terms are considered "further1063 restrictions" within the meaning of section 10. If the Program as you1064 received it, or any part of it, contains a notice stating that it is1065 governed by this License along with a term that is a further1066 restriction, you may remove that term. If a license document contains1067 a further restriction but permits relicensing or conveying under this1068 License, you may add to a covered work material governed by the terms1069 of that license document, provided that the further restriction does1070 not survive such relicensing or conveying.1071 1072 If you add terms to a covered work in accord with this section, you1073 must place, in the relevant source files, a statement of the1074 additional terms that apply to those files, or a notice indicating1075 where to find the applicable terms.1076 1077 Additional terms, permissive or non-permissive, may be stated in the1078 form of a separately written license, or stated as exceptions;1079 the above requirements apply either way.1080 1081 8. Termination.1082 1083 You may not propagate or modify a covered work except as expressly1084 provided under this License. Any attempt otherwise to propagate or1085 modify it is void, and will automatically terminate your rights under1086 this License (including any patent licenses granted under the third1087 paragraph of section 11).1088 1089 However, if you cease all violation of this License, then your1090 license from a particular copyright holder is reinstated (a)1091 provisionally, unless and until the copyright holder explicitly and1092 finally terminates your license, and (b) permanently, if the copyright1093 holder fails to notify you of the violation by some reasonable means1094 prior to 60 days after the cessation.1095 1096 Moreover, your license from a particular copyright holder is1097 reinstated permanently if the copyright holder notifies you of the1098 violation by some reasonable means, this is the first time you have1099 received notice of violation of this License (for any work) from that1100 copyright holder, and you cure the violation prior to 30 days after1101 your receipt of the notice.1102 1103 Termination of your rights under this section does not terminate the1104 licenses of parties who have received copies or rights from you under1105 this License. If your rights have been terminated and not permanently1106 reinstated, you do not qualify to receive new licenses for the same1107 material under section 10.1108 1109 9. Acceptance Not Required for Having Copies.1110 1111 You are not required to accept this License in order to receive or1112 run a copy of the Program. Ancillary propagation of a covered work1113 occurring solely as a consequence of using peer-to-peer transmission1114 to receive a copy likewise does not require acceptance. However,1115 nothing other than this License grants you permission to propagate or1116 modify any covered work. These actions infringe copyright if you do1117 not accept this License. Therefore, by modifying or propagating a1118 covered work, you indicate your acceptance of this License to do so.1119 1120 10. Automatic Licensing of Downstream Recipients.1121 1122 Each time you convey a covered work, the recipient automatically1123 receives a license from the original licensors, to run, modify and1124 propagate that work, subject to this License. You are not responsible1125 for enforcing compliance by third parties with this License.1126 1127 An "entity transaction" is a transaction transferring control of an1128 organization, or substantially all assets of one, or subdividing an1129 organization, or merging organizations. If propagation of a covered1130 work results from an entity transaction, each party to that1131 transaction who receives a copy of the work also receives whatever1132 licenses to the work the party's predecessor in interest had or could1133 give under the previous paragraph, plus a right to possession of the1134 Corresponding Source of the work from the predecessor in interest, if1135 the predecessor has it or can get it with reasonable efforts.1136 1137 You may not impose any further restrictions on the exercise of the1138 rights granted or affirmed under this License. For example, you may1139 not impose a license fee, royalty, or other charge for exercise of1140 rights granted under this License, and you may not initiate litigation1141 (including a cross-claim or counterclaim in a lawsuit) alleging that1142 any patent claim is infringed by making, using, selling, offering for1143 sale, or importing the Program or any portion of it.1144 1145 11. Patents.1146 1147 A "contributor" is a copyright holder who authorizes use under this1148 License of the Program or a work on which the Program is based. The1149 work thus licensed is called the contributor's "contributor version".1150 1151 A contributor's "essential patent claims" are all patent claims1152 owned or controlled by the contributor, whether already acquired or1153 hereafter acquired, that would be infringed by some manner, permitted1154 by this License, of making, using, or selling its contributor version,1155 but do not include claims that would be infringed only as a1156 consequence of further modification of the contributor version. For1157 purposes of this definition, "control" includes the right to grant1158 patent sublicenses in a manner consistent with the requirements of1159 this License.1160 1161 Each contributor grants you a non-exclusive, worldwide, royalty-free1162 patent license under the contributor's essential patent claims, to1163 make, use, sell, offer for sale, import and otherwise run, modify and1164 propagate the contents of its contributor version.1165 1166 In the following three paragraphs, a "patent license" is any express1167 agreement or commitment, however denominated, not to enforce a patent1168 (such as an express permission to practice a patent or covenant not to1169 sue for patent infringement). To "grant" such a patent license to a1170 party means to make such an agreement or commitment not to enforce a1171 patent against the party.1172 1173 If you convey a covered work, knowingly relying on a patent license,1174 and the Corresponding Source of the work is not available for anyone1175 to copy, free of charge and under the terms of this License, through a1176 publicly available network server or other readily accessible means,1177 then you must either (1) cause the Corresponding Source to be so1178 available, or (2) arrange to deprive yourself of the benefit of the1179 patent license for this particular work, or (3) arrange, in a manner1180 consistent with the requirements of this License, to extend the patent1181 license to downstream recipients. "Knowingly relying" means you have1182 actual knowledge that, but for the patent license, your conveying the1183 covered work in a country, or your recipient's use of the covered work1184 in a country, would infringe one or more identifiable patents in that1185 country that you have reason to believe are valid.1186 1187 If, pursuant to or in connection with a single transaction or1188 arrangement, you convey, or propagate by procuring conveyance of, a1189 covered work, and grant a patent license to some of the parties1190 receiving the covered work authorizing them to use, propagate, modify1191 or convey a specific copy of the covered work, then the patent license1192 you grant is automatically extended to all recipients of the covered1193 work and works based on it.1194 1195 A patent license is "discriminatory" if it does not include within1196 the scope of its coverage, prohibits the exercise of, or is1197 conditioned on the non-exercise of one or more of the rights that are1198 specifically granted under this License. You may not convey a covered1199 work if you are a party to an arrangement with a third party that is1200 in the business of distributing software, under which you make payment1201 to the third party based on the extent of your activity of conveying1202 the work, and under which the third party grants, to any of the1203 parties who would receive the covered work from you, a discriminatory1204 patent license (a) in connection with copies of the covered work1205 conveyed by you (or copies made from those copies), or (b) primarily1206 for and in connection with specific products or compilations that1207 contain the covered work, unless you entered into that arrangement,1208 or that patent license was granted, prior to 28 March 2007.1209 1210 Nothing in this License shall be construed as excluding or limiting1211 any implied license or other defenses to infringement that may1212 otherwise be available to you under applicable patent law.1213 1214 12. No Surrender of Others' Freedom.1215 1216 If conditions are imposed on you (whether by court order, agreement or1217 otherwise) that contradict the conditions of this License, they do not1218 excuse you from the conditions of this License. If you cannot convey a1219 covered work so as to satisfy simultaneously your obligations under this1220 License and any other pertinent obligations, then as a consequence you may1221 not convey it at all. For example, if you agree to terms that obligate you1222 to collect a royalty for further conveying from those to whom you convey1223 the Program, the only way you could satisfy both those terms and this1224 License would be to refrain entirely from conveying the Program.1225 1226 13. Use with the GNU Affero General Public License.1227 1228 Notwithstanding any other provision of this License, you have1229 permission to link or combine any covered work with a work licensed1230 under version 3 of the GNU Affero General Public License into a single1231 combined work, and to convey the resulting work. The terms of this1232 License will continue to apply to the part which is the covered work,1233 but the special requirements of the GNU Affero General Public License,1234 section 13, concerning interaction through a network will apply to the1235 combination as such.1236 1237 14. Revised Versions of this License.1238 1239 The Free Software Foundation may publish revised and/or new versions of1240 the GNU General Public License from time to time. Such new versions will1241 be similar in spirit to the present version, but may differ in detail to1242 address new problems or concerns.1243 1244 Each version is given a distinguishing version number. If the1245 Program specifies that a certain numbered version of the GNU General1246 Public License "or any later version" applies to it, you have the1247 option of following the terms and conditions either of that numbered1248 version or of any later version published by the Free Software1249 Foundation. If the Program does not specify a version number of the1250 GNU General Public License, you may choose any version ever published1251 by the Free Software Foundation.1252 1253 If the Program specifies that a proxy can decide which future1254 versions of the GNU General Public License can be used, that proxy's1255 public statement of acceptance of a version permanently authorizes you1256 to choose that version for the Program.1257 1258 Later license versions may give you additional or different1259 permissions. However, no additional obligations are imposed on any1260 author or copyright holder as a result of your choosing to follow a1261 later version.1262 1263 15. Disclaimer of Warranty.1264 1265 THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY1266 APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT1267 HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY1268 OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,1269 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR1270 PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM1271 IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF1272 ALL NECESSARY SERVICING, REPAIR OR CORRECTION.1273 1274 16. Limitation of Liability.1275 1276 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING1277 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS1278 THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY1279 GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE1280 USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF1281 DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD1282 PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),1283 EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF1284 SUCH DAMAGES.1285 1286 17. Interpretation of Sections 15 and 16.1287 1288 If the disclaimer of warranty and limitation of liability provided1289 above cannot be given local legal effect according to their terms,1290 reviewing courts shall apply local law that most closely approximates1291 an absolute waiver of all civil liability in connection with the1292 Program, unless a warranty or assumption of liability accompanies a1293 copy of the Program in return for a fee.1294 1295 END OF TERMS AND CONDITIONS1296 1297 How to Apply These Terms to Your New Programs1298 1299 If you develop a new program, and you want it to be of the greatest1300 possible use to the public, the best way to achieve this is to make it1301 free software which everyone can redistribute and change under these terms.1302 1303 To do so, attach the following notices to the program. It is safest1304 to attach them to the start of each source file to most effectively1305 state the exclusion of warranty; and each file should have at least1306 the "copyright" line and a pointer to where the full notice is found.1307 1308 <one line to give the program's name and a brief idea of what it does.>1309 Copyright (C) <year> <name of author>1310 1311 This program is free software: you can redistribute it and/or modify1312 it under the terms of the GNU General Public License as published by1313 the Free Software Foundation, either version 3 of the License, or1314 (at your option) any later version.1315 1316 This program is distributed in the hope that it will be useful,1317 but WITHOUT ANY WARRANTY; without even the implied warranty of1318 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the1319 GNU General Public License for more details.1320 1321 You should have received a copy of the GNU General Public License1322 along with this program. If not, see <http://www.gnu.org/licenses/>.1323 1324 Also add information on how to contact you by electronic and paper mail.1325 1326 If the program does terminal interaction, make it output a short1327 notice like this when it starts in an interactive mode:1328 1329 <program> Copyright (C) <year> <name of author>1330 This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.1331 This is free software, and you are welcome to redistribute it1332 under certain conditions; type `show c' for details.1333 1334 The hypothetical commands `show w' and `show c' should show the appropriate1335 parts of the General Public License. Of course, your program's commands1336 might be different; for a GUI interface, you would use an "about box".1337 1338 You should also get your employer (if you work as a programmer) or school,1339 if any, to sign a "copyright disclaimer" for the program, if necessary.1340 For more information on this, and how to apply and follow the GNU GPL, see1341 <http://www.gnu.org/licenses/>.1342 1343 The GNU General Public License does not permit incorporating your program1344 into proprietary programs. If your program is a subroutine library, you1345 may consider it more useful to permit linking proprietary applications with1346 the library. If this is what you want to do, use the GNU Lesser General1347 Public License instead of this License. But first, please read1348 <http://www.gnu.org/philosophy/why-not-lgpl.html>. -
branches/stable/DNSDB.pm
r544 r545 3 3 ## 4 4 # $Id$ 5 # Copyright 2008-201 1Kris Deugau <kdeugau@deepnet.cx>5 # Copyright 2008-2012 Kris Deugau <kdeugau@deepnet.cx> 6 6 # 7 7 # This program is free software: you can redistribute it and/or modify … … 30 30 use NetAddr::IP qw(:lower); 31 31 use POSIX; 32 use Fcntl qw(:flock); 33 32 34 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); 33 35 34 $VERSION = "1.0.5"; ##VERSION##36 $VERSION = 1.1; ##VERSION## 35 37 @ISA = qw(Exporter); 36 38 @EXPORT_OK = qw( 37 &initGlobals 39 &initGlobals &login &initActionLog 38 40 &initPermissions &getPermissions &changePermissions &comparePermissions 39 41 &changeGroup 40 42 &loadConfig &connectDB &finish 41 &addDomain &del Domain &domainName &revName &domainID &addRDNS42 &getZoneCount &getZoneList 43 &addDomain &delZone &domainName &revName &domainID &revID &addRDNS 44 &getZoneCount &getZoneList &getZoneLocation 43 45 &addGroup &delGroup &getChildren &groupName 46 &getGroupCount &getGroupList 44 47 &addUser &updateUser &delUser &userFullName &userStatus &getUserData 45 &getSOA &getRecLine &getDomRecs &getRecCount 48 &getUserCount &getUserList &getUserDropdown 49 &addLoc &updateLoc &delLoc &getLoc 50 &getLocCount &getLocList &getLocDropdown 51 &getSOA &updateSOA &getRecLine &getDomRecs &getRecCount 46 52 &addRec &updateRec &delRec 53 &getLogCount &getLogEntries 47 54 &getTypelist 48 55 &parentID 49 56 &isParent 50 & domStatus &importAXFR57 &zoneStatus &importAXFR 51 58 &export 52 59 &mailNotify 53 60 %typemap %reverse_typemap %config 54 %permissions @permtypes $permlist 61 %permissions @permtypes $permlist %permchains 55 62 ); 56 63 57 64 @EXPORT = (); # Export nothing by default. 58 65 %EXPORT_TAGS = ( ALL => [qw( 59 &initGlobals 66 &initGlobals &login &initActionLog 60 67 &initPermissions &getPermissions &changePermissions &comparePermissions 61 68 &changeGroup 62 69 &loadConfig &connectDB &finish 63 &addDomain &del Domain &domainName &revName &domainID &addRDNS64 &getZoneCount &getZoneList 70 &addDomain &delZone &domainName &revName &domainID &revID &addRDNS 71 &getZoneCount &getZoneList &getZoneLocation 65 72 &addGroup &delGroup &getChildren &groupName 73 &getGroupCount &getGroupList 66 74 &addUser &updateUser &delUser &userFullName &userStatus &getUserData 67 &getSOA &getRecLine &getDomRecs &getRecCount 75 &getUserCount &getUserList &getUserDropdown 76 &addLoc &updateLoc &delLoc &getLoc 77 &getLocCount &getLocList &getLocDropdown 78 &getSOA &updateSOA &getRecLine &getDomRecs &getRecCount 68 79 &addRec &updateRec &delRec 80 &getLogCount &getLogEntries 69 81 &getTypelist 70 82 &parentID 71 83 &isParent 72 & domStatus &importAXFR84 &zoneStatus &importAXFR 73 85 &export 74 86 &mailNotify 75 87 %typemap %reverse_typemap %config 76 %permissions @permtypes $permlist 88 %permissions @permtypes $permlist %permchains 77 89 )] 78 90 ); … … 80 92 our $group = 1; 81 93 our $errstr = ''; 94 our $resultstr = ''; 82 95 83 96 # Halfway sane defaults for SOA, TTL, etc. … … 97 110 98 111 # Arguably defined wholly in the db, but little reason to change without supporting code changes 112 # group_view, user_view permissions? separate rDNS permission(s)? 99 113 our @permtypes = qw ( 100 114 group_edit group_create group_delete 101 115 user_edit user_create user_delete 102 116 domain_edit domain_create domain_delete 103 record_edit record_create record_delete 117 record_edit record_create record_delete record_locchg 118 location_edit location_create location_delete location_view 104 119 self_edit admin 105 120 ); 106 121 our $permlist = join(',',@permtypes); 122 123 # Some permissions more or less require certain others. 124 our %permchains = ( 125 user_edit => 'self_edit', 126 location_edit => 'location_view', 127 location_create => 'location_view', 128 location_delete => 'location_view', 129 record_locchg => 'location_view', 130 ); 107 131 108 132 # DNS record type map and reverse map. … … 135 159 # cssdir => 'templates/', 136 160 sessiondir => 'session/', 161 exportcache => 'cache/', 137 162 138 163 # Session params … … 145 170 146 171 ## (Semi)private variables 172 147 173 # Hash of functions for validating record types. Filled in initGlobals() since 148 174 # it relies on visibility flags from the rectypes table in the DB 149 175 my %validators; 150 176 151 152 ## 153 ## utility functions 154 # _rectable() 155 # Takes default+rdns flags, returns appropriate table name 156 sub _rectable { 157 my $def = shift; 158 my $rev = shift; 159 160 return 'records' if $def ne 'y'; 161 return 'default_records' if $rev ne 'y'; 162 return 'default_rev_records'; 163 } # end _rectable() 164 165 # _recparent() 166 # Takes default+rdns flags, returns appropriate parent-id column name 167 sub _recparent { 168 my $def = shift; 169 my $rev = shift; 170 171 return 'group_id' if $def eq 'y'; 172 return 'rdns_id' if $rev eq 'y'; 173 return 'domain_id'; 174 } # end _recparent() 175 176 # Check an IP to be added in a reverse zone to see if it's really in the requested parent. 177 # Takes a database handle, default and reverse flags, IP (fragment) to check, parent zone ID, 178 # and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for 179 # database insertion) 180 sub _ipparent { 181 my $dbh = shift; 182 my $defrec = shift; 183 my $revrec = shift; 184 my $val = shift; 185 my $id = shift; 186 my $addr = shift; 187 188 return if $revrec ne 'y'; # this sub not useful in forward zones 189 190 $$addr = NetAddr::IP->new($$val); #necessary? 191 192 # subsub to split, reverse, and overlay an IP fragment on a netblock 193 sub __rev_overlay { 194 my $splitme = shift; # ':' or '.', m'lud? 195 my $parnet = shift; 196 my $val = shift; 197 my $addr = shift; 198 199 my $joinme = $splitme; 200 $splitme = '\.' if $splitme eq '.'; 201 my @working = reverse(split($splitme, $parnet->addr)); 202 my @parts = reverse(split($splitme, $$val)); 203 for (my $i = 0; $i <= $#parts; $i++) { 204 $working[$i] = $parts[$i]; 205 } 206 my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0; 207 return 0 unless $checkme->within($parnet); 208 $$addr = $checkme; # force "correct" IP to be recorded. 209 return 1; 210 } 211 212 my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id)); 213 my $parnet = NetAddr::IP->new($parstr); 214 215 # Fail early on v6-in-v4 or v4-in-v6. We're not accepting these ATM. 216 return 0 if $parnet->addr =~ /\./ && $$val =~ /:/; 217 return 0 if $parnet->addr =~ /:/ && $$val =~ /\./; 218 219 if ($$addr && $$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/) { 220 # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address. 221 # the rest we have to restructure before fiddling. *sigh* 222 return 1 if $$addr->within($parnet); 223 } else { 224 # We don't have a complete IP in $$val (yet) 225 if ($parnet->addr =~ /:/) { 226 $$val =~ s/^:+//; # gotta strip'em all... 227 return __rev_overlay(':', $parnet, $val, $addr); 228 } 229 if ($parnet->addr =~ /\./) { 230 $$val =~ s/^\.+//; 231 return __rev_overlay('.', $parnet, $val, $addr); 232 } 233 # should be impossible to get here... 234 } 235 # ... and here. 236 # can't do nuttin' in forward zones 237 } # end _ipparent() 238 239 # A little different than _ipparent above; this tries to *find* the parent zone of a hostname 240 sub _hostparent { 241 my $dbh = shift; 242 my $hname = shift; 243 244 my @hostbits = split /\./, $hname; 245 my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE domain = ? GROUP BY domain_id"); 246 foreach (@hostbits) { 247 $sth->execute($hname); 248 my ($found, $parid) = $sth->fetchrow_array; 249 if ($found) { 250 return $parid; 251 } 252 $hname =~ s/^$_\.//; 253 } 254 } # end _hostparent() 255 256 ## 257 ## Record validation subs. 258 ## 259 260 # A record 261 sub _validate_1 { 262 my $dbh = shift; 263 264 my %args = @_; 265 266 return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y'; 267 268 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 269 # or the intended parent domain for live records. 270 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 271 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 272 273 # Check IP is well-formed, and that it's a v4 address 274 # Fail on "compact" IPv4 variants, because they are not consistent and predictable. 275 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") 276 unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/; 277 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") 278 unless $args{addr} && !$args{addr}->{isv6}; 279 # coerce IP/value to normalized form for storage 280 ${$args{val}} = $args{addr}->addr; 281 282 return ('OK','OK'); 283 } # done A record 284 285 # NS record 286 sub _validate_2 { 287 my $dbh = shift; 288 289 my %args = @_; 290 291 # Coerce the hostname to "DOMAIN" for forward default records, "ZONE" for reverse default records, 292 # or the intended parent zone for live records. 293 ##fixme: allow for delegating <subdomain>.DOMAIN? 294 if ($args{revrec} eq 'y') { 295 my $pname = ($args{defrec} eq 'y' ? 'ZONE' : revName($dbh,$args{id})); 296 ${$args{host}} = $pname if ${$args{host}} ne $pname; 297 } else { 298 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 299 ${$args{host}} = $pname if ${$args{host}} ne $pname; 300 } 301 302 # Let this lie for now. Needs more magic. 303 # # Check IP is well-formed, and that it's a v4 address 304 # return ('FAIL',"A record must be a valid IPv4 address") 305 # unless $addr && !$addr->{isv6}; 306 # # coerce IP/value to normalized form for storage 307 # $$val = $addr->addr; 308 309 return ('OK','OK'); 310 } # done NS record 311 312 # CNAME record 313 sub _validate_5 { 314 my $dbh = shift; 315 316 my %args = @_; 317 318 # Not really true, but these are only useful for delegating smaller-than-/24 IP blocks. 319 # This is fundamentally a messy operation and should really just be taken care of by the 320 # export process, not manual maintenance of the necessary records. 321 return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y'; 322 323 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 324 # or the intended parent domain for live records. 325 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 326 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 327 328 return ('OK','OK'); 329 } # done CNAME record 330 331 # SOA record 332 sub _validate_6 { 333 # Smart monkeys won't stick their fingers in here; we have 334 # separate dedicated routines to deal with SOA records. 335 return ('OK','OK'); 336 } # done SOA record 337 338 # PTR record 339 sub _validate_12 { 340 my $dbh = shift; 341 342 my %args = @_; 343 344 if ($args{revrec} eq 'y') { 345 if ($args{defrec} eq 'n') { 346 return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id})) 347 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr}); 348 ${$args{val}} = $args{addr}->addr; 349 } else { 350 if (${$args{val}} =~ /\./) { 351 # looks like a v4 or fragment 352 if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) { 353 # woo! a complete IP! validate it and normalize, or fail. 354 $args{addr} = NetAddr::IP->new(${$args{val}}) 355 or return ('FAIL', "IP/value looks like IPv4 but isn't valid"); 356 ${$args{val}} = $args{addr}->addr; 357 } else { 358 ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/; 359 } 360 } elsif (${$args{val}} =~ /[a-f:]/) { 361 # looks like a v6 or fragment 362 ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/; 363 if ($args{addr}) { 364 if ($args{addr}->addr =~ /^0/) { 365 ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/; 366 } else { 367 ${$args{val}} = $args{addr}->addr; 368 } 369 } 370 } else { 371 # bare number (probably). These could be v4 or v6, so we'll 372 # expand on these on creation of a reverse zone. 373 ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/; 374 } 375 ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/; 376 } 377 378 # Multiple PTR records do NOT generally do what most people believe they do, 379 # and tend to fail in the most awkward way possible. Check and warn. 380 # We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12" 381 382 my @checkvals = (${$args{val}}); 383 if (${$args{val}} =~ /,/) { 384 # push . and :: variants into checkvals if val has , 385 my $tmp; 386 ($tmp = ${$args{val}}) =~ s/,/./; 387 push @checkvals, $tmp; 388 ($tmp = ${$args{val}}) =~ s/,/::/; 389 push @checkvals, $tmp; 390 } 391 my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?"); 392 foreach my $checkme (@checkvals) { 393 my $ptrcount; 394 ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 395 " WHERE val = ?", undef, ($checkme)); 396 return ('WARN', "PTR record for $checkme already exists; adding another will probably not do what you want") 397 if $ptrcount; 398 } 399 } else { 400 # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations 401 # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct 402 # PTR records on export 403 return ('FAIL',"Forward zones cannot contain PTR records"); 404 } 405 406 return ('OK','OK'); 407 } # done PTR record 408 409 # MX record 410 sub _validate_15 { 411 my $dbh = shift; 412 413 my %args = @_; 414 415 # Not absolutely true but WTF use is an MX record for a reverse zone? 416 return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y'; 417 418 return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}}); 419 ${$args{dist}} =~ s/\s*//g; 420 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; 421 422 ${$args{fields}} = "distance,"; 423 push @{$args{vallist}}, ${$args{dist}}; 424 425 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 426 # or the intended parent domain for live records. 427 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 428 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 429 430 return ('OK','OK'); 431 } # done MX record 432 433 # TXT record 434 sub _validate_16 { 435 # Could arguably put a WARN return here on very long (>512) records 436 return ('OK','OK'); 437 } # done TXT record 438 439 # RP record 440 sub _validate_17 { 441 # Probably have to validate these some day 442 return ('OK','OK'); 443 } # done RP record 444 445 # AAAA record 446 sub _validate_28 { 447 my $dbh = shift; 448 449 my %args = @_; 450 451 return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y'; 452 453 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 454 # or the intended parent domain for live records. 455 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 456 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 457 458 # Check IP is well-formed, and that it's a v6 address 459 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address") 460 unless $args{addr} && $args{addr}->{isv6}; 461 # coerce IP/value to normalized form for storage 462 ${$args{val}} = $args{addr}->addr; 463 464 return ('OK','OK'); 465 } # done AAAA record 466 467 # SRV record 468 sub _validate_33 { 469 my $dbh = shift; 470 471 my %args = @_; 472 473 # Not absolutely true but WTF use is an SRV record for a reverse zone? 474 return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y'; 475 476 return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}}); 477 ${$args{dist}} =~ s/\s*//g; 478 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; 479 480 return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]") 481 unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/; 482 return ('FAIL',"Port and weight are required for SRV records") 483 unless defined(${$args{weight}}) && defined(${$args{port}}); 484 ${$args{weight}} =~ s/\s*//g; 485 ${$args{port}} =~ s/\s*//g; 486 487 return ('FAIL',"Port and weight are required, and must be numeric") 488 unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/; 489 490 ${$args{fields}} = "distance,weight,port,"; 491 push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}}); 492 493 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 494 # or the intended parent domain for live records. 495 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 496 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 497 498 return ('OK','OK'); 499 } # done SRV record 500 501 # Now the custom types 502 503 # A+PTR record. With a very little bit of magic we can also use this sub to validate AAAA+PTR. Whee! 504 sub _validate_65280 { 505 my $dbh = shift; 506 507 my %args = @_; 508 509 my $code = 'OK'; 510 my $msg = 'OK'; 511 512 if ($args{defrec} eq 'n') { 513 # live record; revrec determines whether we validate the PTR or A component first. 514 515 if ($args{revrec} eq 'y') { 516 ($code,$msg) = _validate_12($dbh, %args); 517 return ($code,$msg) if $code eq 'FAIL'; 518 519 # Check if the reqested domain exists. If not, coerce the type down to PTR and warn. 520 if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) { 521 my $addmsg = "Record added as PTR instead of $typemap{${$args{rectype}}}; domain not found for ${$args{host}}"; 522 $msg .= "\n$addmsg" if $code eq 'WARN'; 523 $msg = $addmsg if $code eq 'OK'; 524 ${$args{rectype}} = $reverse_typemap{PTR}; 525 return ('WARN', $msg); 526 } 527 528 # Add domain ID to field list and values 529 ${$args{fields}} .= "domain_id,"; 530 push @{$args{vallist}}, ${$args{domid}}; 531 532 } else { 533 ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280; 534 ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281; 535 return ($code,$msg) if $code eq 'FAIL'; 536 537 # Check if the requested reverse zone exists - note, an IP fragment won't 538 # work here since we don't *know* which parent to put it in. 539 # ${$args{val}} has been validated as a valid IP by now, in one of the above calls. 540 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?". 541 " ORDER BY masklen(revnet) DESC", undef, (${$args{val}})); 542 if (!$revid) { 543 $msg = "Record added as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA'). 544 " instead of $typemap{${$args{rectype}}}; reverse zone not found for ${$args{val}}"; 545 ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA}); 546 return ('WARN', $msg); 547 } 548 549 # Check for duplicate PTRs. Note we don't have to play games with $code and $msg, because 550 # by definition there can't be duplicate PTRs if the reverse zone isn't managed here. 551 my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 552 " WHERE val = ?", undef, ${$args{val}}); 553 if ($ptrcount) { 554 $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want"; 555 $code = 'WARN'; 556 } 557 558 ${$args{fields}} .= "rdns_id,"; 559 push @{$args{vallist}}, $revid; 560 } 561 562 } else { # defrec eq 'y' 563 if ($args{revrec} eq 'y') { 564 ($code,$msg) = _validate_12($dbh, %args); 565 return ($code,$msg) if $code eq 'FAIL'; 566 if (${$args{rectype}} == 65280) { 567 return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment") 568 if ${$args{val}} =~ /:/; 569 ${$args{val}} =~ s/^ZONE,/ZONE./; # Clean up after uncertain IP-fragment-type from _validate_12 570 } elsif (${$args{rectype}} == 65281) { 571 return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment") 572 if ${$args{val}} =~ /\./; 573 ${$args{val}} =~ s/^ZONE,/ZONE::/; # Clean up after uncertain IP-fragment-type from _validate_12 574 } 575 } else { 576 # This is easy. I also can't see a real use-case for A/AAAA+PTR in *all* forward 577 # domains, since you wouldn't be able to substitute both domain and reverse zone 578 # sanely, and you'd end up with guaranteed over-replicated PTR records that would 579 # confuse the hell out of pretty much anything that uses them. 580 ##fixme: make this a config flag? 581 return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains"); 582 } 583 } 584 585 return ($code, $msg); 586 } # done A+PTR record 587 588 # AAAA+PTR record 589 # A+PTR above has been magicked to handle AAAA+PTR as well. 590 sub _validate_65281 { 591 return _validate_65280(@_); 592 } # done AAAA+PTR record 593 594 # PTR template record 595 sub _validate_65282 { 596 return ('OK','OK'); 597 } # done PTR template record 598 599 # A+PTR template record 600 sub _validate_65283 { 601 return ('OK','OK'); 602 } # done AAAA+PTR template record 603 604 # AAAA+PTR template record 605 sub _validate_65284 { 606 return ('OK','OK'); 607 } # done AAAA+PTR template record 608 609 610 611 ## 612 ## Initialization and cleanup subs 613 ## 614 615 616 ## DNSDB::loadConfig() 617 # Load the minimum required initial state (DB connect info) from a config file 618 # Load misc other bits while we're at it. 619 # Takes an optional basename and config path to look for 620 # Populates the %config and %def hashes 621 sub loadConfig { 622 my $basename = shift || ''; # this will work OK 623 ##fixme $basename isn't doing what I think I thought I was trying to do. 624 625 my $deferr = ''; # place to put error from default config file in case we can't find either one 626 627 my $configroot = "/etc/dnsdb"; ##CFG_LEAF## 628 $configroot = '' if $basename =~ m|^/|; 629 $basename .= ".conf" if $basename !~ /\.conf$/; 630 my $defconfig = "$configroot/dnsdb.conf"; 631 my $siteconfig = "$configroot/$basename"; 632 633 # System defaults 634 __cfgload("$defconfig") or $deferr = $errstr; 635 636 # Per-site-ish settings. 637 if ($basename ne '.conf') { 638 unless (__cfgload("$siteconfig")) { 639 $errstr = ($deferr ? "Error opening default config file $defconfig: $deferr\n" : ''). 640 "Error opening site config file $siteconfig"; 641 return; 642 } 643 } 644 645 # Munge log_failures. 646 if ($config{log_failures} ne '1' && $config{log_failures} ne '0') { 647 # true/false, on/off, yes/no all valid. 648 if ($config{log_failures} =~ /^(?:true|false|on|off|yes|no)$/) { 649 if ($config{log_failures} =~ /(?:true|on|yes)/) { 650 $config{log_failures} = 1; 651 } else { 652 $config{log_failures} = 0; 653 } 654 } else { 655 $errstr = "Bad log_failures setting $config{log_failures}"; 656 $config{log_failures} = 1; 657 # Bad setting shouldn't be fatal. 658 # return 2; 659 } 660 } 661 662 # All good, clear the error and go home. 663 $errstr = ''; 664 return 1; 665 } # end loadConfig() 666 667 668 ## DNSDB::__cfgload() 669 # Private sub to parse a config file and load it into %config 670 # Takes a file handle on an open config file 671 sub __cfgload { 672 $errstr = ''; 673 my $cfgfile = shift; 674 675 if (open CFG, "<$cfgfile") { 676 while (<CFG>) { 677 chomp; 678 s/^\s*//; 679 next if /^#/; 680 next if /^$/; 681 # hmm. more complex bits in this file might require [heading] headers, maybe? 682 # $mode = $1 if /^\[(a-z)+]/; 683 # DB connect info 684 $config{dbname} = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i; 685 $config{dbuser} = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i; 686 $config{dbpass} = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i; 687 $config{dbhost} = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i; 688 # SOA defaults 689 $def{contact} = $1 if /^contact\s*=\s*([a-z0-9_.-]+)/i; 690 $def{prins} = $1 if /^prins\s*=\s*([a-z0-9_.-]+)/i; 691 $def{soattl} = $1 if /^soattl\s*=\s*(\d+)/i; 692 $def{refresh} = $1 if /^refresh\s*=\s*(\d+)/i; 693 $def{retry} = $1 if /^retry\s*=\s*(\d+)/i; 694 $def{expire} = $1 if /^expire\s*=\s*(\d+)/i; 695 $def{minttl} = $1 if /^minttl\s*=\s*(\d+)/i; 696 $def{ttl} = $1 if /^ttl\s*=\s*(\d+)/i; 697 # Mail settings 698 $config{mailhost} = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i; 699 $config{mailnotify} = $1 if /^mailnotify\s*=\s*([a-z0-9_.\@-]+)/i; 700 $config{mailsender} = $1 if /^mailsender\s*=\s*([a-z0-9_.\@-]+)/i; 701 $config{mailname} = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i; 702 $config{orgname} = $1 if /^orgname\s*=\s*([a-z0-9\s_.,'-]+)/i; 703 $config{domain} = $1 if /^domain\s*=\s*([a-z0-9_.-]+)/i; 704 # session - note this is fed directly to CGI::Session 705 $config{timeout} = $1 if /^[tT][iI][mM][eE][oO][uU][tT]\s*=\s*(\d+[smhdwMy]?)/; 706 $config{sessiondir} = $1 if m{^sessiondir\s*=\s*([a-z0-9/_.-]+)}i; 707 # misc 708 $config{log_failures} = $1 if /^log_failures\s*=\s*([a-z01]+)/i; 709 $config{perpage} = $1 if /^perpage\s*=\s*(\d+)/i; 710 } 711 close CFG; 712 } else { 713 $errstr = $!; 714 return; 715 } 716 return 1; 717 } # end __cfgload() 718 719 720 ## DNSDB::connectDB() 721 # Creates connection to DNS database. 722 # Requires the database name, username, and password. 723 # Returns a handle to the db. 724 # Set up for a PostgreSQL db; could be any transactional DBMS with the 725 # right changes. 726 sub connectDB { 727 $errstr = ''; 728 my $dbname = shift; 729 my $user = shift; 730 my $pass = shift; 731 my $dbh; 732 my $DSN = "DBI:Pg:dbname=$dbname"; 733 734 my $host = shift; 735 $DSN .= ";host=$host" if $host; 736 737 # Note that we want to autocommit by default, and we will turn it off locally as necessary. 738 # We may not want to print gobbledygook errors; YMMV. Have to ponder that further. 739 $dbh = DBI->connect($DSN, $user, $pass, { 740 AutoCommit => 1, 741 PrintError => 0 742 }) 743 or return (undef, $DBI::errstr) if(!$dbh); 744 745 ##fixme: initialize the DB if we can't find the table (since, by definition, there's 746 # nothing there if we can't select from it...) 747 my $tblsth = $dbh->prepare("SELECT count(*) FROM pg_catalog.pg_class WHERE relkind='r' AND relname=?"); 748 my ($tblcount) = $dbh->selectrow_array($tblsth, undef, ('misc')); 749 return (undef,$DBI::errstr) if $dbh->err; 750 751 #if ($tblcount == 0) { 752 # # create tables one at a time, checking for each. 753 # return (undef, "check table misc missing"); 754 #} 755 756 757 # Return here if we can't select. 758 # This should retrieve the dbversion key. 759 my $sth = $dbh->prepare("SELECT key,value FROM misc WHERE misc_id=1"); 760 $sth->execute(); 761 return (undef,$DBI::errstr) if ($sth->err); 762 763 ##fixme: do stuff to the DB on version mismatch 764 # x.y series should upgrade on $DNSDB::VERSION > misc(key=>version) 765 # DB should be downward-compatible; column defaults should give sane (if possibly 766 # useless-and-needs-help) values in columns an older software stack doesn't know about. 767 768 # See if the select returned anything (or null data). This should 769 # succeed if the select executed, but... 770 $sth->fetchrow(); 771 return (undef,$DBI::errstr) if ($sth->err); 772 773 $sth->finish; 774 775 # If we get here, we should be OK. 776 return ($dbh,"DB connection OK"); 777 } # end connectDB 778 779 780 ## DNSDB::finish() 781 # Cleans up after database handles and so on. 782 # Requires a database handle 783 sub finish { 784 my $dbh = $_[0]; 785 $dbh->disconnect; 786 } # end finish 787 788 789 ## DNSDB::initGlobals() 790 # Initialize global variables 791 # NB: this does NOT include web-specific session variables! 792 # Requires a database handle 793 sub initGlobals { 794 my $dbh = shift; 795 796 # load record types from database 797 my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes"); 798 $sth->execute; 799 while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) { 800 $typemap{$recval} = $recname; 801 $reverse_typemap{$recname} = $recval; 802 # now we fill the record validation function hash 803 if ($stdflag < 5) { 804 my $fn = "_validate_$recval"; 805 $validators{$recval} = \&$fn; 806 } else { 807 my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }"; 808 $validators{$recval} = eval $fn; 809 } 810 } 811 } # end initGlobals 812 813 814 ## DNSDB::initPermissions() 815 # Set up permissions global 816 # Takes database handle and UID 817 sub initPermissions { 818 my $dbh = shift; 819 my $uid = shift; 820 821 # %permissions = $(getPermissions($dbh,'user',$uid)); 822 getPermissions($dbh, 'user', $uid, \%permissions); 823 824 } # end initPermissions() 825 826 827 ## DNSDB::getPermissions() 828 # Get permissions from DB 829 # Requires DB handle, group or user flag, ID, and hashref. 830 sub getPermissions { 831 my $dbh = shift; 832 my $type = shift; 833 my $id = shift; 834 my $hash = shift; 835 836 my $sql = qq( 837 SELECT 838 p.admin,p.self_edit, 839 p.group_create,p.group_edit,p.group_delete, 840 p.user_create,p.user_edit,p.user_delete, 841 p.domain_create,p.domain_edit,p.domain_delete, 842 p.record_create,p.record_edit,p.record_delete 843 FROM permissions p 844 ); 845 if ($type eq 'group') { 846 $sql .= qq( 847 JOIN groups g ON g.permission_id=p.permission_id 848 WHERE g.group_id=? 849 ); 850 } else { 851 $sql .= qq( 852 JOIN users u ON u.permission_id=p.permission_id 853 WHERE u.user_id=? 854 ); 855 } 856 857 my $sth = $dbh->prepare($sql); 858 859 $sth->execute($id) or die "argh: ".$sth->errstr; 860 861 # my $permref = $sth->fetchrow_hashref; 862 # return $permref; 863 # $hash = $permref; 864 # Eww. Need to learn how to forcibly drop a hashref onto an existing hash. 865 ($hash->{admin},$hash->{self_edit}, 866 $hash->{group_create},$hash->{group_edit},$hash->{group_delete}, 867 $hash->{user_create},$hash->{user_edit},$hash->{user_delete}, 868 $hash->{domain_create},$hash->{domain_edit},$hash->{domain_delete}, 869 $hash->{record_create},$hash->{record_edit},$hash->{record_delete}) 870 = $sth->fetchrow_array; 871 872 } # end getPermissions() 873 874 875 ## DNSDB::changePermissions() 876 # Update an ACL entry 877 # Takes a db handle, type, owner-id, and hashref for the changed permissions. 878 sub changePermissions { 879 my $dbh = shift; 880 my $type = shift; 881 my $id = shift; 882 my $newperms = shift; 883 my $inherit = shift || 0; 884 885 my $failmsg = ''; 886 887 # see if we're switching from inherited to custom. for bonus points, 888 # snag the permid and parent permid anyway, since we'll need the permid 889 # to set/alter custom perms, and both if we're switching from custom to 890 # inherited. 891 my $sth = $dbh->prepare("SELECT (u.permission_id=g.permission_id) AS was_inherited,u.permission_id,g.permission_id". 892 " FROM ".($type eq 'user' ? 'users' : 'groups')." u ". 893 " JOIN groups g ON u.".($type eq 'user' ? '' : 'parent_')."group_id=g.group_id ". 894 " WHERE u.".($type eq 'user' ? 'user' : 'group')."_id=?"); 895 $sth->execute($id); 896 897 my ($wasinherited,$permid,$parpermid) = $sth->fetchrow_array; 898 899 # hack phtoui 900 # group id 1 is "special" in that it's it's own parent (err... possibly.) 901 # may make its parent id 0 which doesn't exist, and as a bonus is Perl-false. 902 $wasinherited = 0 if ($type eq 'group' && $id == 1); 903 904 local $dbh->{AutoCommit} = 0; 905 local $dbh->{RaiseError} = 1; 906 907 # Wrap all the SQL in a transaction 908 eval { 909 if ($inherit) { 910 911 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='t',permission_id=? ". 912 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($parpermid, $id) ); 913 $dbh->do("DELETE FROM permissions WHERE permission_id=?", undef, ($permid) ); 914 915 } else { 916 917 if ($wasinherited) { # munge new permission entry in if we're switching from inherited perms 918 ##fixme: need to add semirecursive bit to properly munge inherited permission ID on subgroups and users 919 # ... if'n'when we have groups with fully inherited permissions. 920 # SQL is coo 921 $dbh->do("INSERT INTO permissions ($permlist,".($type eq 'user' ? 'user' : 'group')."_id) ". 922 "SELECT $permlist,? FROM permissions WHERE permission_id=?", undef, ($id,$permid) ); 923 ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions ". 924 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($id) ); 925 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='f',permission_id=? ". 926 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($permid, $id) ); 927 } 928 929 # and now set the permissions we were passed 930 foreach (@permtypes) { 931 if (defined ($newperms->{$_})) { 932 $dbh->do("UPDATE permissions SET $_=? WHERE permission_id=?", undef, ($newperms->{$_},$permid) ); 933 } 934 } 935 936 } # (inherited->)? custom 937 938 $dbh->commit; 939 }; # end eval 940 if ($@) { 941 my $msg = $@; 942 eval { $dbh->rollback; }; 943 return ('FAIL',"$failmsg: $msg ($permid)"); 944 } else { 945 return ('OK',$permid); 946 } 947 948 } # end changePermissions() 949 950 951 ## DNSDB::comparePermissions() 952 # Compare two permission hashes 953 # Returns '>', '<', '=', '!' 954 sub comparePermissions { 955 my $p1 = shift; 956 my $p2 = shift; 957 958 my $retval = '='; # assume equality until proven otherwise 959 960 no warnings "uninitialized"; 961 962 foreach (@permtypes) { 963 next if $p1->{$_} == $p2->{$_}; # equal is good 964 if ($p1->{$_} && !$p2->{$_}) { 965 if ($retval eq '<') { # if we've already found an unequal pair where 966 $retval = '!'; # $p2 has more access, and we now find a pair 967 last; # where $p1 has more access, the overall access 968 } # is neither greater or lesser, it's unequal. 969 $retval = '>'; 970 } 971 if (!$p1->{$_} && $p2->{$_}) { 972 if ($retval eq '>') { # if we've already found an unequal pair where 973 $retval = '!'; # $p1 has more access, and we now find a pair 974 last; # where $p2 has more access, the overall access 975 } # is neither greater or lesser, it's unequal. 976 $retval = '<'; 977 } 978 } 979 return $retval; 980 } # end comparePermissions() 981 982 983 ## DNSDB::changeGroup() 984 # Change group ID of an entity 985 # Takes a database handle, entity type, entity ID, and new group ID 986 sub changeGroup { 987 my $dbh = shift; 988 my $type = shift; 989 my $id = shift; 990 my $newgrp = shift; 991 992 ##fixme: fail on not enough args 993 #return ('FAIL', "Missing 994 995 if ($type eq 'domain') { 996 $dbh->do("UPDATE domains SET group_id=? WHERE domain_id=?", undef, ($newgrp, $id)) 997 or return ('FAIL','Group change failed: '.$dbh->errstr); 998 } elsif ($type eq 'user') { 999 $dbh->do("UPDATE users SET group_id=? WHERE user_id=?", undef, ($newgrp, $id)) 1000 or return ('FAIL','Group change failed: '.$dbh->errstr); 1001 } elsif ($type eq 'group') { 1002 $dbh->do("UPDATE groups SET parent_group_id=? WHERE group_id=?", undef, ($newgrp, $id)) 1003 or return ('FAIL','Group change failed: '.$dbh->errstr); 1004 } 1005 return ('OK','OK'); 1006 } # end changeGroup() 1007 1008 1009 ## DNSDB::_log() 1010 # Log an action 1011 # Internal sub 1012 # Takes a database handle and log entry hash containing at least: 1013 # user_id, group_id, log entry 1014 # and optionally one or more of: 1015 # username/email, user full name, domain_id, rdns_id 1016 ##fixme: convert to trailing hash for user info 1017 # User info must contain a (user ID OR username)+fullname 1018 sub _log { 1019 my $dbh = shift; 1020 1021 my %args = @_; 1022 1023 $args{rdns_id} = 0 if !$args{rdns_id}; 1024 $args{domain_id} = 0 if !$args{domain_id}; 1025 1026 ##fixme: need better way(s?) to snag userinfo for log entries. don't want to have 1027 # to pass around yet *another* constant (already passing $dbh, shouldn't need to) 1028 my $fullname; 1029 if (!$args{user_id}) { 1030 ($args{user_id}, $fullname) = $dbh->selectrow_array("SELECT user_id, firstname || ' ' || lastname FROM users". 1031 " WHERE username=?", undef, ($args{username})); 1032 } 1033 if (!$args{username}) { 1034 ($args{username}, $fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname FROM users". 1035 " WHERE user_id=?", undef, ($args{user_id})); 1036 } 1037 if (!$args{fullname}) { 1038 ($fullname) = $dbh->selectrow_array("SELECT firstname || ' ' || lastname FROM users". 1039 " WHERE user_id=?", undef, ($args{user_id})); 1040 } 1041 1042 $args{name} = $fullname if !$args{name}; 1043 1044 ##fixme: farm out the actual logging to different subs for file, syslog, internal, etc based on config 1045 $dbh->do("INSERT INTO log (domain_id,rdns_id,user_id,group_id,email,name,entry) VALUES (?,?,?,?,?,?,?)", 1046 undef, 1047 ($args{domain_id},$args{rdns_id},$args{user_id},$args{group_id},$args{username},$args{name},$args{entry})); 1048 1049 } # end _log 1050 1051 1052 ## 1053 ## Processing subs 1054 ## 1055 1056 ## DNSDB::addDomain() 1057 # Add a domain 1058 # Takes a database handle, domain name, numeric group, boolean(ish) state (active/inactive), 1059 # and user info hash (for logging). 1060 # Returns a status code and message 1061 sub addDomain { 1062 $errstr = ''; 1063 my $dbh = shift; 1064 return ('FAIL',"Need database handle") if !$dbh; 1065 my $domain = shift; 1066 return ('FAIL',"Domain must not be blank") if !$domain; 1067 my $group = shift; 1068 return ('FAIL',"Need group") if !defined($group); 1069 my $state = shift; 1070 return ('FAIL',"Need domain status") if !defined($state); 1071 1072 my %userinfo = @_; # remaining bits. 1073 # user ID, username, user full name 1074 1075 $state = 1 if $state =~ /^active$/; 1076 $state = 1 if $state =~ /^on$/; 1077 $state = 0 if $state =~ /^inactive$/; 1078 $state = 0 if $state =~ /^off$/; 1079 1080 return ('FAIL',"Invalid domain status") if $state !~ /^\d+$/; 1081 1082 return ('FAIL', "Invalid characters in domain") if $domain !~ /^[a-zA-Z0-9_.-]+$/; 1083 1084 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?"); 1085 my $dom_id; 1086 1087 # quick check to start to see if we've already got one 1088 $sth->execute($domain); 1089 ($dom_id) = $sth->fetchrow_array; 1090 1091 return ('FAIL', "Domain already exists") if $dom_id; 1092 1093 # Allow transactions, and raise an exception on errors so we can catch it later. 1094 # Use local to make sure these get "reset" properly on exiting this block 1095 local $dbh->{AutoCommit} = 0; 1096 local $dbh->{RaiseError} = 1; 1097 1098 # Wrap all the SQL in a transaction 1099 eval { 1100 # insert the domain... 1101 $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($domain, $group, $state)); 1102 1103 # get the ID... 1104 ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain)); 1105 1106 _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{username}, 1107 entry => "Added ".($state ? 'active' : 'inactive')." domain $domain")); 1108 1109 # ... and now we construct the standard records from the default set. NB: group should be variable. 1110 my $sth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?"); 1111 my $sth_in = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,distance,weight,port,ttl)". 1112 " VALUES ($dom_id,?,?,?,?,?,?,?)"); 1113 $sth->execute($group); 1114 while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $sth->fetchrow_array()) { 1115 $host =~ s/DOMAIN/$domain/g; 1116 $val =~ s/DOMAIN/$domain/g; 1117 $sth_in->execute($host,$type,$val,$dist,$weight,$port,$ttl); 1118 if ($typemap{$type} eq 'SOA') { 1119 my @tmp1 = split /:/, $host; 1120 my @tmp2 = split /:/, $val; 1121 _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, 1122 username => $userinfo{username}, entry => 1123 "[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 1124 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); 1125 } else { 1126 my $logentry = "[new $domain] Added record '$host $typemap{$type}"; 1127 $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX'; 1128 $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV'; 1129 _log($dbh, (domain_id => $dom_id, user_id => $userinfo{id}, group_id => $group, 1130 username => $userinfo{username}, entry => 1131 $logentry." $val', TTL $ttl")); 1132 } 1133 } 1134 1135 # once we get here, we should have suceeded. 1136 $dbh->commit; 1137 }; # end eval 1138 1139 if ($@) { 1140 my $msg = $@; 1141 eval { $dbh->rollback; }; 1142 return ('FAIL',$msg); 1143 } else { 1144 return ('OK',$dom_id); 1145 } 1146 } # end addDomain 1147 1148 1149 ## DNSDB::delDomain() 1150 # Delete a domain. 1151 # for now, just delete the records, then the domain. 1152 # later we may want to archive it in some way instead (status code 2, for example?) 1153 sub delDomain { 1154 my $dbh = shift; 1155 my $domid = shift; 1156 1157 # Allow transactions, and raise an exception on errors so we can catch it later. 1158 # Use local to make sure these get "reset" properly on exiting this block 1159 local $dbh->{AutoCommit} = 0; 1160 local $dbh->{RaiseError} = 1; 1161 1162 my $failmsg = ''; 1163 1164 # Wrap all the SQL in a transaction 1165 eval { 1166 my $sth = $dbh->prepare("delete from records where domain_id=?"); 1167 $failmsg = "Failure removing domain records"; 1168 $sth->execute($domid); 1169 $sth = $dbh->prepare("delete from domains where domain_id=?"); 1170 $failmsg = "Failure removing domain"; 1171 $sth->execute($domid); 1172 1173 # once we get here, we should have suceeded. 1174 $dbh->commit; 1175 }; # end eval 1176 1177 if ($@) { 1178 my $msg = $@; 1179 eval { $dbh->rollback; }; 1180 return ('FAIL',"$failmsg: $msg"); 1181 } else { 1182 return ('OK','OK'); 1183 } 1184 1185 } # end delDomain() 1186 1187 1188 ## DNSDB::domainName() 1189 # Return the domain name based on a domain ID 1190 # Takes a database handle and the domain ID 1191 # Returns the domain name or undef on failure 1192 sub domainName { 1193 $errstr = ''; 1194 my $dbh = shift; 1195 my $domid = shift; 1196 my ($domname) = $dbh->selectrow_array("SELECT domain FROM domains WHERE domain_id=?", undef, ($domid) ); 1197 $errstr = $DBI::errstr if !$domname; 1198 return $domname if $domname; 1199 } # end domainName() 1200 1201 1202 ## DNSDB::revName() 1203 # Return the reverse zone name based on an rDNS ID 1204 # Takes a database handle and the rDNS ID 1205 # Returns the reverse zone name or undef on failure 1206 sub revName { 1207 $errstr = ''; 1208 my $dbh = shift; 1209 my $revid = shift; 1210 my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) ); 1211 $errstr = $DBI::errstr if !$revname; 1212 return $revname if $revname; 1213 } # end revName() 1214 1215 1216 ## DNSDB::domainID() 1217 # Takes a database handle and domain name 1218 # Returns the domain ID number 1219 sub domainID { 1220 $errstr = ''; 1221 my $dbh = shift; 1222 my $domain = shift; 1223 my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE domain=?", undef, ($domain) ); 1224 $errstr = $DBI::errstr if !$domid; 1225 return $domid if $domid; 1226 } # end domainID() 1227 1228 1229 ## DNSDB::addRDNS 1230 # Adds a reverse DNS zone 1231 # Takes a database handle, CIDR block, numeric group, boolean(ish) state (active/inactive), 1232 # and user info hash (for logging). 1233 # Returns a status code and message 1234 sub addRDNS { 1235 my $dbh = shift; 1236 my $zone = NetAddr::IP->new(shift); 1237 return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/); 1238 my $revpatt = shift; 1239 my $group = shift; 1240 my $state = shift; 1241 1242 my %userinfo = @_; # remaining bits. 1243 # user ID, username, user full name 1244 1245 $state = 1 if $state =~ /^active$/; 1246 $state = 1 if $state =~ /^on$/; 1247 $state = 0 if $state =~ /^inactive$/; 1248 $state = 0 if $state =~ /^off$/; 1249 1250 return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/; 1251 1252 # quick check to start to see if we've already got one 1253 my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revzone=?", undef, ("$zone")); 1254 1255 return ('FAIL', "Zone already exists") if $rdns_id; 1256 1257 # Allow transactions, and raise an exception on errors so we can catch it later. 1258 # Use local to make sure these get "reset" properly on exiting this block 1259 local $dbh->{AutoCommit} = 0; 1260 local $dbh->{RaiseError} = 1; 1261 1262 #$dbh->selectrow_array("SELECT currval('users_user_id_seq')"); 1263 # Wrap all the SQL in a transaction 1264 eval { 1265 # insert the domain... 1266 $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($zone, $group, $state)); 1267 1268 # get the ID... 1269 ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); 1270 1271 _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, username => $userinfo{name}, 1272 entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone")); 1273 1274 # ... and now we construct the standard records from the default set. NB: group should be variable. 1275 my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?"); 1276 my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,host,type,val,ttl)". 1277 " VALUES ($rdns_id,?,?,?,?)"); 1278 $sth->execute($group); 1279 while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) { 1280 $host =~ s/ADMINDOMAIN/$config{domain}/g; 1281 ##work 1282 # - replace ZONE in $val 1283 # - skip records not appropriate for the zone (skip A+PTR on v6 zones, and AAAA+PTR on v4 zones) 1284 # $val =~ s/DOMAIN/$domain/g; 1285 $sth_in->execute($host,$type,$val,$ttl); 1286 if ($typemap{$type} eq 'SOA') { 1287 my @tmp1 = split /:/, $host; 1288 my @tmp2 = split /:/, $val; 1289 _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, 1290 username => $userinfo{name}, entry => 1291 "[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 1292 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); 1293 } else { 1294 my $logentry = "[new $zone] Added record '$host $typemap{$type}"; 1295 # $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX'; 1296 # $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV'; 1297 _log($dbh, (rdns_id => $rdns_id, user_id => $userinfo{id}, group_id => $group, 1298 username => $userinfo{name}, entry => 1299 $logentry." $val', TTL $ttl")); 1300 } 1301 } 1302 1303 # once we get here, we should have suceeded. 1304 $dbh->commit; 1305 }; # end eval 1306 1307 if ($@) { 1308 my $msg = $@; 1309 eval { $dbh->rollback; }; 1310 return ('FAIL',$msg); 1311 } else { 1312 return ('OK',$rdns_id); 1313 } 1314 1315 } # end addRDNS() 1316 1317 1318 ## DNSDB::getZoneCount 1319 # Get count of zones in group or groups 1320 # Takes a database handle and hash containing: 1321 # - the "current" group 1322 # - an array of "acceptable" groups 1323 # - a flag for forward/reverse zones 1324 # - Optionally accept a "starts with" and/or "contains" filter argument 1325 # Returns an integer count of the resulting zone list. 1326 sub getZoneCount { 1327 my $dbh = shift; 1328 1329 my %args = @_; 1330 1331 my @filterargs; 1332 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 1333 push @filterargs, "^$args{startwith}" if $args{startwith}; 1334 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones 1335 push @filterargs, $args{filter} if $args{filter}; 1336 1337 my $sql; 1338 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read 1339 if ($args{revrec} eq 'n') { 1340 $sql = "SELECT count(*) FROM domains". 1341 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1342 ($args{startwith} ? " AND domain ~* ?" : ''). 1343 ($args{filter} ? " AND domain ~* ?" : ''); 1344 } else { 1345 $sql = "SELECT count(*) FROM revzones". 1346 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1347 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). 1348 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); 1349 } 1350 my ($count) = $dbh->selectrow_array($sql, undef, @filterargs); 1351 return $count; 1352 } # end getZoneCount() 1353 1354 1355 ## DNSDB::getZoneList() 1356 # Get a list of zones in the specified group(s) 1357 # Takes the same arguments as getZoneCount() above 1358 # Returns a reference to an array of hashrefs suitable for feeding to HTML::Template 1359 sub getZoneList { 1360 my $dbh = shift; 1361 1362 my %args = @_; 1363 1364 my @zonelist; 1365 1366 $args{sortorder} = 'ASC' if !grep $args{sortorder}, ('ASC','DESC'); 1367 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 1368 1369 my @filterargs; 1370 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 1371 push @filterargs, "^$args{startwith}" if $args{startwith}; 1372 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones 1373 push @filterargs, $args{filter} if $args{filter}; 1374 1375 my $sql; 1376 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read 1377 if ($args{revrec} eq 'n') { 1378 $args{sortby} = 'domain' if !grep $args{sortby}, ('revnet','group','status'); 1379 $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains". 1380 " INNER JOIN groups ON domains.group_id=groups.group_id". 1381 " WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1382 ($args{startwith} ? " AND domain ~* ?" : ''). 1383 ($args{filter} ? " AND domain ~* ?" : ''); 1384 } else { 1385 ##fixme: arguably startwith here is irrelevant. depends on the UI though. 1386 $args{sortby} = 'revnet' if !grep $args{sortby}, ('domain','group','status'); 1387 $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones". 1388 " INNER JOIN groups ON revzones.group_id=groups.group_id". 1389 " WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 1390 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). 1391 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); 1392 } 1393 # A common tail. 1394 $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ". 1395 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}". 1396 " OFFSET ".$args{offset}*$config{perpage}); 1397 my $sth = $dbh->prepare($sql); 1398 $sth->execute(@filterargs); 1399 my $rownum = 0; 1400 1401 while (my @data = $sth->fetchrow_array) { 1402 my %row; 1403 $row{domainid} = $data[0]; 1404 $row{domain} = $data[1]; 1405 $row{status} = $data[2]; 1406 $row{group} = $data[3]; 1407 push @zonelist, \%row; 1408 } 1409 1410 return \@zonelist; 1411 } # end getZoneList() 1412 1413 1414 ## DNSDB::addGroup() 1415 # Add a group 1416 # Takes a database handle, group name, parent group, hashref for permissions, 1417 # and optional template-vs-cloneme flag 1418 # Returns a status code and message 1419 sub addGroup { 1420 $errstr = ''; 1421 my $dbh = shift; 1422 my $groupname = shift; 1423 my $pargroup = shift; 1424 my $permissions = shift; 1425 1426 # 0 indicates "custom", hardcoded. 1427 # Any other value clones that group's default records, if it exists. 1428 my $inherit = shift || 0; 1429 ##fixme: need a flag to indicate clone records or <?> ? 1430 1431 # Allow transactions, and raise an exception on errors so we can catch it later. 1432 # Use local to make sure these get "reset" properly on exiting this block 1433 local $dbh->{AutoCommit} = 0; 1434 local $dbh->{RaiseError} = 1; 1435 1436 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE group_name=?"); 1437 my $group_id; 1438 1439 # quick check to start to see if we've already got one 1440 $sth->execute($groupname); 1441 ($group_id) = $sth->fetchrow_array; 1442 1443 return ('FAIL', "Group already exists") if $group_id; 1444 1445 # Wrap all the SQL in a transaction 1446 eval { 1447 $sth = $dbh->prepare("INSERT INTO groups (parent_group_id,group_name) VALUES (?,?)"); 1448 $sth->execute($pargroup,$groupname); 1449 1450 my ($groupid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname)); 1451 1452 # Permissions 1453 if ($inherit) { 1454 } else { 1455 my @permvals; 1456 foreach (@permtypes) { 1457 if (!defined ($permissions->{$_})) { 1458 push @permvals, 0; 1459 } else { 1460 push @permvals, $permissions->{$_}; 1461 } 1462 } 1463 1464 $sth = $dbh->prepare("INSERT INTO permissions (group_id,$permlist) values (?".',?'x($#permtypes+1).")"); 1465 $sth->execute($groupid,@permvals); 1466 1467 $sth = $dbh->prepare("SELECT permission_id FROM permissions WHERE group_id=?"); 1468 $sth->execute($groupid); 1469 my ($permid) = $sth->fetchrow_array(); 1470 1471 $dbh->do("UPDATE groups SET permission_id=$permid WHERE group_id=$groupid"); 1472 } # done permission fiddling 1473 1474 # Default records 1475 my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ". 1476 "VALUES ($groupid,?,?,?,?,?,?,?)"); 1477 my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ". 1478 "VALUES ($groupid,?,?,?,?)"); 1479 if ($inherit) { 1480 # Duplicate records from parent. Actually relying on inherited records feels 1481 # very fragile, and it would be problematic to roll over at a later time. 1482 my $sth2 = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?"); 1483 $sth2->execute($pargroup); 1484 while (my @clonedata = $sth2->fetchrow_array) { 1485 $sthf->execute(@clonedata); 1486 } 1487 # And now the reverse records 1488 $sth2 = $dbh->prepare("SELECT group_id,host,type,val,ttl FROM default_rev_records WHERE group_id=?"); 1489 $sth2->execute($pargroup); 1490 while (my @clonedata = $sth2->fetchrow_array) { 1491 $sthr->execute(@clonedata); 1492 } 1493 } else { 1494 ##fixme: Hardcoding is Bad, mmmmkaaaay? 1495 # reasonable basic defaults for SOA, MX, NS, and minimal hosting 1496 # could load from a config file, but somewhere along the line we need hardcoded bits. 1497 $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400); 1498 $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200); 1499 $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200); 1500 $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200); 1501 $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200); 1502 $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200); 1503 # reasonable basic defaults for generic reverse zone. Same as initial SQL tabledef. 1504 $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400); 1505 $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600); 1506 } 1507 1508 # once we get here, we should have suceeded. 1509 $dbh->commit; 1510 }; # end eval 1511 1512 if ($@) { 1513 my $msg = $@; 1514 eval { $dbh->rollback; }; 1515 return ('FAIL',$msg); 1516 } else { 1517 return ('OK','OK'); 1518 } 1519 1520 } # end addGroup() 1521 1522 1523 ## DNSDB::delGroup() 1524 # Delete a group. 1525 # Takes a group ID 1526 # Returns a status code and message 1527 sub delGroup { 1528 my $dbh = shift; 1529 my $groupid = shift; 1530 1531 # Allow transactions, and raise an exception on errors so we can catch it later. 1532 # Use local to make sure these get "reset" properly on exiting this block 1533 local $dbh->{AutoCommit} = 0; 1534 local $dbh->{RaiseError} = 1; 1535 1536 ##fixme: locate "knowable" error conditions and deal with them before the eval 1537 # ... or inside, whatever. 1538 # -> domains still exist in group 1539 # -> ... 1540 my $failmsg = ''; 1541 1542 # Wrap all the SQL in a transaction 1543 eval { 1544 my $sth = $dbh->prepare("SELECT count(*) FROM domains WHERE group_id=?"); 1545 $sth->execute($groupid); 1546 my ($domcnt) = $sth->fetchrow_array; 1547 $failmsg = "Can't remove group ".groupName($dbh,$groupid); 1548 die "$domcnt domains still in group\n" if $domcnt; 1549 1550 $sth = $dbh->prepare("delete from default_records where group_id=?"); 1551 $failmsg = "Failed to delete default records for ".groupName($dbh,$groupid); 1552 $sth->execute($groupid); 1553 $sth = $dbh->prepare("delete from groups where group_id=?"); 1554 $failmsg = "Failed to remove group ".groupName($dbh,$groupid); 1555 $sth->execute($groupid); 1556 1557 # once we get here, we should have suceeded. 1558 $dbh->commit; 1559 }; # end eval 1560 1561 if ($@) { 1562 my $msg = $@; 1563 eval { $dbh->rollback; }; 1564 return ('FAIL',"$failmsg: $msg"); 1565 } else { 1566 return ('OK','OK'); 1567 } 1568 } # end delGroup() 1569 1570 1571 ## DNSDB::getChildren() 1572 # Get a list of all groups whose parent^n is group <n> 1573 # Takes a database handle, group ID, reference to an array to put the group IDs in, 1574 # and an optional flag to return only immediate children or all children-of-children 1575 # default to returning all children 1576 # Calls itself 1577 sub getChildren { 1578 $errstr = ''; 1579 my $dbh = shift; 1580 my $rootgroup = shift; 1581 my $groupdest = shift; 1582 my $immed = shift || 'all'; 1583 1584 # special break for default group; otherwise we get stuck. 1585 if ($rootgroup == 1) { 1586 # by definition, group 1 is the Root Of All Groups 1587 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE NOT (group_id=1)". 1588 ($immed ne 'all' ? " AND parent_group_id=1" : '')); 1589 $sth->execute; 1590 while (my @this = $sth->fetchrow_array) { 1591 push @$groupdest, @this; 1592 } 1593 } else { 1594 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE parent_group_id=?"); 1595 $sth->execute($rootgroup); 1596 return if $sth->rows == 0; 1597 my @grouplist; 1598 while (my ($group) = $sth->fetchrow_array) { 1599 push @$groupdest, $group; 1600 getChildren($dbh,$group,$groupdest) if $immed eq 'all'; 1601 } 1602 } 1603 } # end getChildren() 1604 1605 1606 ## DNSDB::groupName() 1607 # Return the group name based on a group ID 1608 # Takes a database handle and the group ID 1609 # Returns the group name or undef on failure 1610 sub groupName { 1611 $errstr = ''; 1612 my $dbh = shift; 1613 my $groupid = shift; 1614 my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?"); 1615 $sth->execute($groupid); 1616 my ($groupname) = $sth->fetchrow_array(); 1617 $errstr = $DBI::errstr if !$groupname; 1618 return $groupname if $groupname; 1619 } # end groupName 1620 1621 1622 ## DNSDB::groupID() 1623 # Return the group ID based on the group name 1624 # Takes a database handle and the group name 1625 # Returns the group ID or undef on failure 1626 sub groupID { 1627 $errstr = ''; 1628 my $dbh = shift; 1629 my $group = shift; 1630 my ($grpid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group=?", undef, ($group) ); 1631 $errstr = $DBI::errstr if !$grpid; 1632 return $grpid if $grpid; 1633 } # end groupID() 1634 1635 1636 ## DNSDB::addUser() 1637 # Add a user. 1638 # Takes a DB handle, username, group ID, password, state (active/inactive). 1639 # Optionally accepts: 1640 # user type (user/admin) - defaults to user 1641 # permissions string - defaults to inherit from group 1642 # three valid forms: 1643 # i - Inherit permissions 1644 # c:<user_id> - Clone permissions from <user_id> 1645 # C:<permission list> - Set these specific permissions 1646 # first name - defaults to username 1647 # last name - defaults to blank 1648 # phone - defaults to blank (could put other data within column def) 1649 # Returns (OK,<uid>) on success, (FAIL,<message>) on failure 1650 sub addUser { 1651 $errstr = ''; 1652 my $dbh = shift; 1653 my $username = shift; 1654 my $group = shift; 1655 my $pass = shift; 1656 my $state = shift; 1657 1658 return ('FAIL', "Missing one or more required entries") if !defined($state); 1659 return ('FAIL', "Username must not be blank") if !$username; 1660 1661 my $type = shift || 'u'; # create limited users by default - fwiw, not sure yet how this will interact with ACLs 1662 1663 my $permstring = shift || 'i'; # default is to inhert permissions from group 1664 1665 my $fname = shift || $username; 1666 my $lname = shift || ''; 1667 my $phone = shift || ''; # not going format-check 1668 1669 my $sth = $dbh->prepare("SELECT user_id FROM users WHERE username=?"); 1670 my $user_id; 1671 1672 # quick check to start to see if we've already got one 1673 $sth->execute($username); 1674 ($user_id) = $sth->fetchrow_array; 1675 1676 return ('FAIL', "User already exists") if $user_id; 1677 1678 # Allow transactions, and raise an exception on errors so we can catch it later. 1679 # Use local to make sure these get "reset" properly on exiting this block 1680 local $dbh->{AutoCommit} = 0; 1681 local $dbh->{RaiseError} = 1; 1682 1683 my $failmsg = ''; 1684 1685 # Wrap all the SQL in a transaction 1686 eval { 1687 # insert the user... note we set inherited perms by default since 1688 # it's simple and cleans up some other bits of state 1689 my $sth = $dbh->prepare("INSERT INTO users ". 1690 "(group_id,username,password,firstname,lastname,phone,type,status,permission_id,inherit_perm) ". 1691 "VALUES (?,?,?,?,?,?,?,?,(SELECT permission_id FROM permissions WHERE group_id=?),'t')"); 1692 $sth->execute($group,$username,unix_md5_crypt($pass),$fname,$lname,$phone,$type,$state,$group); 1693 1694 # get the ID... 1695 ($user_id) = $dbh->selectrow_array("SELECT currval('users_user_id_seq')"); 1696 1697 # Permissions! Gotta set'em all! 1698 die "Invalid permission string $permstring" 1699 if $permstring !~ /^(?: 1700 i # inherit 1701 |c:\d+ # clone 1702 # custom. no, the leading , is not a typo 1703 |C:(?:,(?:group|user|domain|record|self)_(?:edit|create|delete))* 1704 )$/x; 1705 # bleh. I'd call another function to do my dirty work, but we're in the middle of a transaction already. 1706 if ($permstring ne 'i') { 1707 # for cloned or custom permissions, we have to create a new permissions entry. 1708 my $clonesrc = $group; 1709 if ($permstring =~ /^c:(\d+)/) { $clonesrc = $1; } 1710 $dbh->do("INSERT INTO permissions ($permlist,user_id) ". 1711 "SELECT $permlist,? FROM permissions WHERE permission_id=". 1712 "(SELECT permission_id FROM permissions WHERE ".($permstring =~ /^c:/ ? 'user' : 'group')."_id=?)", 1713 undef, ($user_id,$clonesrc) ); 1714 $dbh->do("UPDATE users SET permission_id=". 1715 "(SELECT permission_id FROM permissions WHERE user_id=?) ". 1716 "WHERE user_id=?", undef, ($user_id, $user_id) ); 1717 } 1718 if ($permstring =~ /^C:/) { 1719 # finally for custom permissions, we set the passed-in permissions (and unset 1720 # any that might have been brought in by the clone operation above) 1721 my ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions WHERE user_id=?", 1722 undef, ($user_id) ); 1723 foreach (@permtypes) { 1724 if ($permstring =~ /,$_/) { 1725 $dbh->do("UPDATE permissions SET $_='t' WHERE permission_id=?", undef, ($permid) ); 1726 } else { 1727 $dbh->do("UPDATE permissions SET $_='f' WHERE permission_id=?", undef, ($permid) ); 1728 } 1729 } 1730 } 1731 1732 $dbh->do("UPDATE users SET inherit_perm='n' WHERE user_id=?", undef, ($user_id) ); 1733 1734 ##fixme: add another table to hold name/email for log table? 1735 1736 # once we get here, we should have suceeded. 1737 $dbh->commit; 1738 }; # end eval 1739 1740 if ($@) { 1741 my $msg = $@; 1742 eval { $dbh->rollback; }; 1743 return ('FAIL',$msg." $failmsg"); 1744 } else { 1745 return ('OK',$user_id); 1746 } 1747 } # end addUser 1748 1749 1750 ## DNSDB::checkUser() 1751 # Check user/pass combo on login 1752 sub checkUser { 1753 my $dbh = shift; 1754 my $user = shift; 1755 my $inpass = shift; 1756 1757 my $sth = $dbh->prepare("SELECT user_id,group_id,password,firstname,lastname FROM users WHERE username=?"); 1758 $sth->execute($user); 1759 my ($uid,$gid,$pass,$fname,$lname) = $sth->fetchrow_array; 1760 my $loginfailed = 1 if !defined($uid); 1761 1762 if ($pass =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) { 1763 $loginfailed = 1 if $pass ne unix_md5_crypt($inpass,$1); 1764 } else { 1765 $loginfailed = 1 if $pass ne $inpass; 1766 } 1767 1768 # nnnngggg 1769 return ($uid, $gid); 1770 } # end checkUser 1771 1772 1773 ## DNSDB:: updateUser() 1774 # Update general data about user 1775 sub updateUser { 1776 my $dbh = shift; 1777 1778 ##fixme: tweak calling convention so that we can update any given bit of data 1779 my $uid = shift; 1780 my $username = shift; 1781 my $group = shift; 1782 my $pass = shift; 1783 my $state = shift; 1784 my $type = shift || 'u'; 1785 my $fname = shift || $username; 1786 my $lname = shift || ''; 1787 my $phone = shift || ''; # not going format-check 1788 1789 my $failmsg = ''; 1790 1791 # Allow transactions, and raise an exception on errors so we can catch it later. 1792 # Use local to make sure these get "reset" properly on exiting this block 1793 local $dbh->{AutoCommit} = 0; 1794 local $dbh->{RaiseError} = 1; 1795 1796 my $sth; 1797 1798 # Password can be left blank; if so we assume there's one on file. 1799 # Actual blank passwords are bad, mm'kay? 1800 if (!$pass) { 1801 $sth = $dbh->prepare("SELECT password FROM users WHERE user_id=?"); 1802 $sth->execute($uid); 1803 ($pass) = $sth->fetchrow_array; 1804 } else { 1805 $pass = unix_md5_crypt($pass); 1806 } 1807 1808 eval { 1809 my $sth = $dbh->prepare(q( 1810 UPDATE users 1811 SET username=?, password=?, firstname=?, lastname=?, phone=?, type=?, status=? 1812 WHERE user_id=? 1813 ) 1814 ); 1815 $sth->execute($username, $pass, $fname, $lname, $phone, $type, $state, $uid); 1816 $dbh->commit; 1817 }; 1818 if ($@) { 1819 my $msg = $@; 1820 eval { $dbh->rollback; }; 1821 return ('FAIL',"$failmsg: $msg"); 1822 } else { 1823 return ('OK','OK'); 1824 } 1825 } # end updateUser() 1826 1827 1828 ## DNSDB::delUser() 1829 # 1830 sub delUser { 1831 my $dbh = shift; 1832 return ('FAIL',"Need database handle") if !$dbh; 1833 my $userid = shift; 1834 return ('FAIL',"Missing userid") if !defined($userid); 1835 1836 my $sth = $dbh->prepare("delete from users where user_id=?"); 1837 $sth->execute($userid); 1838 1839 return ('FAIL',"Couldn't remove user: ".$sth->errstr) if $sth->err; 1840 1841 return ('OK','OK'); 1842 1843 } # end delUser 1844 1845 1846 ## DNSDB::userFullName() 1847 # Return a pretty string! 1848 # Takes a user_id and optional printf-ish string to indicate which pieces where: 1849 # %u for the username 1850 # %f for the first name 1851 # %l for the last name 1852 # All other text in the passed string will be left as-is. 1853 ##fixme: need a "smart" option too, so that missing/null/blank first/last names don't give funky output 1854 sub userFullName { 1855 $errstr = ''; 1856 my $dbh = shift; 1857 my $userid = shift; 1858 my $fullformat = shift || '%f %l (%u)'; 1859 my $sth = $dbh->prepare("select username,firstname,lastname from users where user_id=?"); 1860 $sth->execute($userid); 1861 my ($uname,$fname,$lname) = $sth->fetchrow_array(); 1862 $errstr = $DBI::errstr if !$uname; 1863 1864 $fullformat =~ s/\%u/$uname/g; 1865 $fullformat =~ s/\%f/$fname/g; 1866 $fullformat =~ s/\%l/$lname/g; 1867 1868 return $fullformat; 1869 } # end userFullName 1870 1871 1872 ## DNSDB::userStatus() 1873 # Sets and/or returns a user's status 1874 # Takes a database handle, user ID and optionally a status argument 1875 # Returns undef on errors. 1876 sub userStatus { 1877 my $dbh = shift; 1878 my $id = shift; 1879 my $newstatus = shift; 1880 1881 return undef if $id !~ /^\d+$/; 1882 1883 my $sth; 1884 1885 # ooo, fun! let's see what we were passed for status 1886 if ($newstatus) { 1887 $sth = $dbh->prepare("update users set status=? where user_id=?"); 1888 # ass-u-me caller knows what's going on in full 1889 if ($newstatus =~ /^[01]$/) { # only two valid for now. 1890 $sth->execute($newstatus,$id); 1891 } elsif ($newstatus =~ /^usero(?:n|ff)$/) { 1892 $sth->execute(($newstatus eq 'useron' ? 1 : 0),$id); 1893 } 1894 } 1895 1896 $sth = $dbh->prepare("select status from users where user_id=?"); 1897 $sth->execute($id); 1898 my ($status) = $sth->fetchrow_array; 1899 return $status; 1900 } # end userStatus() 1901 1902 1903 ## DNSDB::getUserData() 1904 # Get misc user data for display 1905 sub getUserData { 1906 my $dbh = shift; 1907 my $uid = shift; 1908 1909 my $sth = $dbh->prepare("SELECT group_id,username,firstname,lastname,phone,type,status,inherit_perm ". 1910 "FROM users WHERE user_id=?"); 1911 $sth->execute($uid); 1912 return $sth->fetchrow_hashref(); 1913 1914 } # end getUserData() 1915 1916 1917 ## DNSDB::getSOA() 1918 # Return all suitable fields from an SOA record in separate elements of a hash 1919 # Takes a database handle, default/live flag, domain/reverse flag, and parent ID 1920 sub getSOA { 1921 $errstr = ''; 1922 my $dbh = shift; 1923 my $def = shift; 1924 my $rev = shift; 1925 my $id = shift; 1926 my %ret; 1927 1928 # (ab)use distance and weight columns to store SOA data? can't for default_rev_records... 1929 # - should really attach serial to the zone parent somewhere 1930 1931 my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev). 1932 " WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}"; 1933 1934 my $sth = $dbh->prepare($sql); 1935 $sth->execute($id); 1936 ##fixme: stick a flag somewhere if the record doesn't exist. by the API, this is an impossible case, but... 1937 1938 my ($recid,$host,$val,$ttl) = $sth->fetchrow_array() or return; 1939 my ($contact,$prins) = split /:/, $host; 1940 my ($refresh,$retry,$expire,$minttl) = split /:/, $val; 1941 1942 $ret{recid} = $recid; 1943 $ret{ttl} = $ttl; 1944 # $ret{serial} = $serial; # ca't use distance for serial with default_rev_records 1945 $ret{prins} = $prins; 1946 $ret{contact} = $contact; 1947 $ret{refresh} = $refresh; 1948 $ret{retry} = $retry; 1949 $ret{expire} = $expire; 1950 $ret{minttl} = $minttl; 1951 1952 return %ret; 1953 } # end getSOA() 1954 1955 1956 ## DNSDB::updateSOA() 1957 # Update the specified SOA record 1958 # Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash 1959 sub updateSOA { 1960 my $dbh = shift; 1961 my $defrec = shift; 1962 my $revrec = shift; 1963 1964 my %soa = @_; 1965 1966 ##fixme: data validation: make sure {recid} is really the SOA for {parent} 1967 my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6"; 1968 $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}", 1969 $soa{ttl}, $soa{recid})); 1970 1971 } # end updateSOA() 1972 1973 1974 ## DNSDB::getRecLine() 1975 # Return all data fields for a zone record in separate elements of a hash 1976 # Takes a database handle, default/live flag, forward/reverse flag, and record ID 1977 sub getRecLine { 1978 $errstr = ''; 1979 my $dbh = shift; 1980 my $defrec = shift; 1981 my $revrec = shift; 1982 my $id = shift; 1983 1984 my $sql = "SELECT record_id,host,type,val,ttl".($revrec eq 'n' ? ',distance,weight,port' : ''). 1985 (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM '). 1986 _rectable($defrec,$revrec)." WHERE record_id=?"; 1987 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) ); 1988 1989 if ($dbh->err) { 1990 $errstr = $DBI::errstr; 1991 return undef; 1992 } 1993 1994 if (!$ret) { 1995 $errstr = "No such record"; 1996 return undef; 1997 } 1998 1999 # explicitly set a parent id 2000 if ($defrec eq 'y') { 2001 $ret->{parid} = $ret->{group_id}; 2002 } else { 2003 $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id}); 2004 # and a secondary if we have a custom type that lives in both a forward and reverse zone 2005 $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279; 2006 } 2007 2008 return $ret; 2009 } 2010 2011 2012 ##fixme: should use above (getRecLine()) to get lines for below? 2013 ## DNSDB::getDomRecs() 2014 # Return records for a domain 2015 # Takes a database handle, default/live flag, group/domain ID, start, 2016 # number of records, sort field, and sort order 2017 # Returns a reference to an array of hashes 2018 sub getDomRecs { 2019 $errstr = ''; 2020 my $dbh = shift; 2021 my $def = shift; 2022 my $rev = shift; 2023 my $id = shift; 2024 my $nrecs = shift || 'all'; 2025 my $nstart = shift || 0; 2026 2027 ## for order, need to map input to column names 2028 my $order = shift || 'host'; 2029 my $direction = shift || 'ASC'; 2030 2031 my $filter = shift || ''; 2032 2033 my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl"; 2034 $sql .= ",r.distance,r.weight,r.port" if $rev eq 'n'; 2035 $sql .= " FROM "._rectable($def,$rev)." r "; 2036 2037 # whee! multisort means just passing comma-separated fields in sortby! 2038 my $newsort = ''; 2039 foreach my $sf (split /,/, $order) { 2040 $sf = "r.$sf"; 2041 $sf =~ s/r\.type/t.alphaorder/; 2042 $newsort .= ",$sf"; 2043 } 2044 $newsort =~ s/^,//; 2045 2046 $sql .= "INNER JOIN rectypes t ON r.type=t.val "; # for sorting by type alphabetically 2047 $sql .= "WHERE "._recparent($def,$rev)." = ?"; 2048 $sql .= " AND NOT r.type=$reverse_typemap{SOA}"; 2049 $sql .= " AND host ~* ?" if $filter; 2050 # use alphaorder column for "correct" ordering of sort-by-type instead of DNS RR type number 2051 $sql .= " ORDER BY $newsort $direction"; 2052 2053 my @bindvars = ($id); 2054 push @bindvars, $filter if $filter; 2055 2056 # just to be ultraparanoid about SQL injection vectors 2057 if ($nstart ne 'all') { 2058 $sql .= " LIMIT ? OFFSET ?"; 2059 push @bindvars, $nrecs; 2060 push @bindvars, ($nstart*$nrecs); 2061 } 2062 my $sth = $dbh->prepare($sql) or warn $dbh->errstr; 2063 $sth->execute(@bindvars) or warn "$sql: ".$sth->errstr; 2064 2065 my @retbase; 2066 while (my $ref = $sth->fetchrow_hashref()) { 2067 push @retbase, $ref; 2068 } 2069 2070 my $ret = \@retbase; 2071 return $ret; 2072 } # end getDomRecs() 2073 2074 2075 ## DNSDB::getRecCount() 2076 # Return count of non-SOA records in zone (or default records in a group) 2077 # Takes a database handle, default/live flag, reverse/forward flag, group/domain ID, 2078 # and optional filtering modifier 2079 # Returns the count 2080 sub getRecCount { 2081 my $dbh = shift; 2082 my $defrec = shift; 2083 my $revrec = shift; 2084 my $id = shift; 2085 my $filter = shift || ''; 2086 2087 # keep the nasties down, since we can't ?-sub this bit. :/ 2088 # note this is chars allowed in DNS hostnames 2089 $filter =~ s/[^a-zA-Z0-9_.:-]//g; 2090 2091 my @bindvars = ($id); 2092 push @bindvars, $filter if $filter; 2093 my $sql = "SELECT count(*) FROM ". 2094 _rectable($defrec,$revrec). 2095 " WHERE "._recparent($defrec,$revrec)."=? ". 2096 "AND NOT type=$reverse_typemap{SOA}". 2097 ($filter ? " AND host ~* ?" : ''); 2098 my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) ); 2099 2100 return $count; 2101 2102 } # end getRecCount() 2103 2104 2105 ## DNSDB::addRec() 2106 # Add a new record to a domain or a group's default records 2107 # Takes a database handle, default/live flag, group/domain ID, 2108 # host, type, value, and TTL 2109 # Some types require additional detail: "distance" for MX and SRV, 2110 # and weight/port for SRV 2111 # Returns a status code and detail message in case of error 2112 ##fixme: pass a hash with the record data, not a series of separate values 2113 sub addRec { 2114 $errstr = ''; 2115 my $dbh = shift; 2116 my $defrec = shift; 2117 my $revrec = shift; 2118 my $id = shift; # parent (group_id for defrecs, rdns_id for reverse records, 2119 # domain_id for domain records) 2120 2121 my $host = shift; 2122 my $rectype = shift; # reference so we can coerce it if "+"-types can't find both zones 2123 my $val = shift; 2124 my $ttl = shift; 2125 2126 # Spaces are evil. 2127 $host =~ s/^\s+//; 2128 $host =~ s/\s+$//; 2129 if ($typemap{$rectype} ne 'TXT') { 2130 # Leading or trailng spaces could be legit in TXT records. 2131 $val =~ s/^\s+//; 2132 $val =~ s/\s+$//; 2133 } 2134 2135 # Validation 2136 my $addr = NetAddr::IP->new($val); 2137 if ($rectype == $reverse_typemap{A}) { 2138 return ('FAIL',$typemap{$rectype}." record must be a valid IPv4 address") 2139 unless $addr && !$addr->{isv6}; 2140 } 2141 if ($rectype == $reverse_typemap{AAAA}) { 2142 return ('FAIL',$typemap{$rectype}." record must be a valid IPv6 address") 2143 unless $addr && $addr->{isv6}; 2144 } 2145 2146 my $domid = 0; 2147 my $revid = 0; 2148 2149 my $retcode = 'OK'; # assume everything will go OK 2150 my $retmsg = ''; 2151 2152 # do simple validation first 2153 return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/; 2154 2155 # Quick check on hostname parts. Note the regex is more forgiving than the error message; 2156 # domain names technically are case-insensitive, and we use printf-like % codes for a couple 2157 # of types. Other things may also be added to validate default records of several flavours. 2158 return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)") 2159 if $defrec eq 'n' && $$host !~ /^[0-9a-z_%.]+$/i; 2160 2161 # Collect these even if we're only doing a simple A record so we can call *any* validation sub 2162 my $dist = shift; 2163 my $port = shift; 2164 my $weight = shift; 2165 2166 my $fields; 2167 my @vallist; 2168 2169 # Call the validation sub for the type requested. 2170 ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id, 2171 host => $host, rectype => $rectype, val => $val, addr => $addr, 2172 dist => \$dist, port => \$port, weight => \$weight, 2173 fields => \$fields, vallist => \@vallist) ); 2174 2175 return ($retcode,$retmsg) if $retcode eq 'FAIL'; 2176 2177 # Set up database fields and bind parameters 2178 $fields .= "host,type,val,ttl,"._recparent($defrec,$revrec); 2179 push @vallist, ($$host,$$rectype,$$val,$ttl,$id); 2180 my $vallen = '?'.(',?'x$#vallist); 2181 2182 # Allow transactions, and raise an exception on errors so we can catch it later. 2183 # Use local to make sure these get "reset" properly on exiting this block 2184 local $dbh->{AutoCommit} = 0; 2185 local $dbh->{RaiseError} = 1; 2186 2187 eval { 2188 $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)", 2189 undef, @vallist); 2190 $dbh->commit; 2191 }; 2192 if ($@) { 2193 my $msg = $@; 2194 eval { $dbh->rollback; }; 2195 return ('FAIL',$msg); 2196 } 2197 2198 return ($retcode, $retmsg); 2199 2200 } # end addRec() 2201 2202 2203 ## DNSDB::updateRec() 2204 # Update a record 2205 sub updateRec { 2206 $errstr = ''; 2207 2208 my $dbh = shift; 2209 my $defrec = shift; 2210 my $id = shift; 2211 2212 # all records have these 2213 my $host = shift; 2214 my $type = shift; 2215 my $val = shift; 2216 my $ttl = shift; 2217 2218 return('FAIL',"Missing standard argument(s)") if !defined($ttl); 2219 2220 # Spaces are evil. 2221 $host =~ s/^\s+//; 2222 $host =~ s/\s+$//; 2223 if ($typemap{$type} ne 'TXT') { 2224 # Leading or trailng spaces could be legit in TXT records. 2225 $val =~ s/^\s+//; 2226 $val =~ s/\s+$//; 2227 } 2228 2229 # only MX and SRV will use these 2230 my $dist = 0; 2231 my $weight = 0; 2232 my $port = 0; 2233 2234 if ($type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) { 2235 $dist = shift; 2236 $dist =~ s/\s+//g; 2237 return ('FAIL',"MX or SRV requires distance") if !defined($dist); 2238 return ('FAIL', "Distance must be numeric") unless $dist =~ /^\d+$/; 2239 if ($type == $reverse_typemap{SRV}) { 2240 $weight = shift; 2241 $weight =~ s/\s+//g; 2242 return ('FAIL',"SRV requires weight") if !defined($weight); 2243 return ('FAIL',"Weight must be numeric") unless $weight =~ /^\d+$/; 2244 $port = shift; 2245 $port =~ s/\s+//g; 2246 return ('FAIL',"SRV requires port") if !defined($port); 2247 return ('FAIL',"Port must be numeric") unless $port =~ /^\d+$/; 2248 } 2249 } 2250 2251 # Enforce IP addresses on A and AAAA types 2252 my $addr = NetAddr::IP->new($val); 2253 if ($type == $reverse_typemap{A}) { 2254 return ('FAIL',$typemap{$type}." record must be a valid IPv4 address") 2255 unless $addr && !$addr->{isv6}; 2256 } 2257 if ($type == $reverse_typemap{AAAA}) { 2258 return ('FAIL',$typemap{$type}." record must be a valid IPv6 address") 2259 unless $addr && $addr->{isv6}; 2260 } 2261 2262 # hmm.. this might work. except possibly for something pointing to "deadbeef.ca". <g> 2263 # if ($type == $reverse_typemap{NS} || $type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) { 2264 # if ($val =~ /^\s*[\da-f:.]+\s*$/) { 2265 # return ('FAIL',"$val is not a valid IP address") if !$addr; 2266 # } 2267 # } 2268 2269 local $dbh->{AutoCommit} = 0; 2270 local $dbh->{RaiseError} = 1; 2271 2272 eval { 2273 $dbh->do("UPDATE ".($defrec eq 'y' ? 'default_' : '')."records ". 2274 "SET host=?,val=?,type=?,ttl=?,distance=?,weight=?,port=? ". 2275 "WHERE record_id=?", undef, ($host, $val, $type, $ttl, $dist, $weight, $port, $id) ); 2276 $dbh->commit; 2277 }; 2278 if ($@) { 2279 my $msg = $@; 2280 $dbh->rollback; 2281 return ('FAIL', $msg); 2282 } 2283 2284 return ('OK','OK'); 2285 } # end updateRec() 2286 2287 2288 ## DNSDB::delRec() 2289 # Delete a record. 2290 sub delRec { 2291 $errstr = ''; 2292 my $dbh = shift; 2293 my $defrec = shift; 2294 my $revrec = shift; 2295 my $id = shift; 2296 2297 my $sth = $dbh->prepare("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?"); 2298 $sth->execute($id); 2299 2300 return ('FAIL',"Couldn't remove record: ".$sth->errstr) if $sth->err; 2301 2302 return ('OK','OK'); 2303 } # end delRec() 2304 2305 2306 # Reference hashes. 177 # Username, full name, ID - mainly for logging 178 my %userdata; 179 180 # Entity-relationship reference hashes. 2307 181 my %par_tbl = ( 2308 182 group => 'groups', … … 2342 216 ); 2343 217 218 ## 219 ## utility functions 220 ## 221 222 ## DNSDB::_rectable() 223 # Takes default+rdns flags, returns appropriate table name 224 sub _rectable { 225 my $def = shift; 226 my $rev = shift; 227 228 return 'records' if $def ne 'y'; 229 return 'default_records' if $rev ne 'y'; 230 return 'default_rev_records'; 231 } # end _rectable() 232 233 ## DNSDB::_recparent() 234 # Takes default+rdns flags, returns appropriate parent-id column name 235 sub _recparent { 236 my $def = shift; 237 my $rev = shift; 238 239 return 'group_id' if $def eq 'y'; 240 return 'rdns_id' if $rev eq 'y'; 241 return 'domain_id'; 242 } # end _recparent() 243 244 ## DNSDB::_ipparent() 245 # Check an IP to be added in a reverse zone to see if it's really in the requested parent. 246 # Takes a database handle, default and reverse flags, IP (fragment) to check, parent zone ID, 247 # and a reference to a NetAddr::IP object (also used to pass back a fully-reconstructed IP for 248 # database insertion) 249 sub _ipparent { 250 my $dbh = shift; 251 my $defrec = shift; 252 my $revrec = shift; 253 my $val = shift; 254 my $id = shift; 255 my $addr = shift; 256 257 return if $revrec ne 'y'; # this sub not useful in forward zones 258 259 $$addr = NetAddr::IP->new($$val); #necessary? 260 261 # subsub to split, reverse, and overlay an IP fragment on a netblock 262 sub __rev_overlay { 263 my $splitme = shift; # ':' or '.', m'lud? 264 my $parnet = shift; 265 my $val = shift; 266 my $addr = shift; 267 268 my $joinme = $splitme; 269 $splitme = '\.' if $splitme eq '.'; 270 my @working = reverse(split($splitme, $parnet->addr)); 271 my @parts = reverse(split($splitme, $$val)); 272 for (my $i = 0; $i <= $#parts; $i++) { 273 $working[$i] = $parts[$i]; 274 } 275 my $checkme = NetAddr::IP->new(join($joinme, reverse(@working))) or return 0; 276 return 0 unless $checkme->within($parnet); 277 $$addr = $checkme; # force "correct" IP to be recorded. 278 return 1; 279 } 280 281 my ($parstr) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id = ?", undef, ($id)); 282 my $parnet = NetAddr::IP->new($parstr); 283 284 # Fail early on v6-in-v4 or v4-in-v6. We're not accepting these ATM. 285 return 0 if $parnet->addr =~ /\./ && $$val =~ /:/; 286 return 0 if $parnet->addr =~ /:/ && $$val =~ /\./; 287 288 if ($$addr && ($$val =~ /^[\da-fA-F][\da-fA-F:]+[\da-fA-F]$/ || $$val =~ m|/\d+$|)) { 289 # the only case where NetAddr::IP's acceptance of legitimate IPs is "correct" is for a proper IPv6 address, 290 # or a netblock (only expected on templates) 291 # the rest we have to restructure before fiddling. *sigh* 292 return 1 if $$addr->within($parnet); 293 } else { 294 # We don't have a complete IP in $$val (yet)... unless we have a netblock 295 if ($parnet->addr =~ /:/) { 296 $$val =~ s/^:+//; # gotta strip'em all... 297 return __rev_overlay(':', $parnet, $val, $addr); 298 } 299 if ($parnet->addr =~ /\./) { 300 $$val =~ s/^\.+//; 301 return __rev_overlay('.', $parnet, $val, $addr); 302 } 303 # should be impossible to get here... 304 } 305 # ... and here. 306 # can't do nuttin' in forward zones 307 } # end _ipparent() 308 309 ## DNSDB::_hostparent() 310 # A little different than _ipparent above; this tries to *find* the parent zone of a hostname 311 # Takes a database handle and hostname. 312 # Returns the domain ID of the parent domain if one was found. 313 sub _hostparent { 314 my $dbh = shift; 315 my $hname = shift; 316 317 $hname =~ s/^\*\.//; # this should be impossible to find in the domains table. 318 my @hostbits = split /\./, $hname; 319 my $sth = $dbh->prepare("SELECT count(*),domain_id FROM domains WHERE lower(domain) = lower(?) GROUP BY domain_id"); 320 foreach (@hostbits) { 321 $sth->execute($hname); 322 my ($found, $parid) = $sth->fetchrow_array; 323 if ($found) { 324 return $parid; 325 } 326 $hname =~ s/^$_\.//; 327 } 328 } # end _hostparent() 329 330 ## DNSDB::_log() 331 # Log an action 332 # Takes a database handle and log entry hash containing at least: 333 # group_id, log entry 334 # and optionally one or more of: 335 # domain_id, rdns_id 336 # The %userdata hash provides the user ID, username, and fullname 337 sub _log { 338 my $dbh = shift; 339 340 my %args = @_; 341 342 $args{rdns_id} = 0 if !$args{rdns_id}; 343 $args{domain_id} = 0 if !$args{domain_id}; 344 345 ##fixme: farm out the actual logging to different subs for file, syslog, internal, etc based on config 346 # if ($config{log_channel} eq 'sql') { 347 $dbh->do("INSERT INTO log (domain_id,rdns_id,group_id,entry,user_id,email,name) VALUES (?,?,?,?,?,?,?)", 348 undef, 349 ($args{domain_id}, $args{rdns_id}, $args{group_id}, $args{entry}, 350 $userdata{userid}, $userdata{username}, $userdata{fullname}) ); 351 # } elsif ($config{log_channel} eq 'file') { 352 # } elsif ($config{log_channel} eq 'syslog') { 353 # } 354 } # end _log 355 356 357 ## 358 ## Record validation subs. 359 ## 360 361 ## All of these subs take substantially the same arguments: 362 # a database handle 363 # a hash containing at least the following keys: 364 # - defrec (default/live flag) 365 # - revrec (forward/reverse flag) 366 # - id (parent entity ID) 367 # - host (hostname) 368 # - rectype 369 # - val (IP, hostname [CNAME/MX/SRV] or text) 370 # - addr (NetAddr::IP object from val. May be undef.) 371 # MX and SRV record validation also expect distance, and SRV records expect weight and port as well. 372 # host, rectype, and addr should be references as these may be modified in validation 373 374 # A record 375 sub _validate_1 { 376 my $dbh = shift; 377 378 my %args = @_; 379 380 return ('FAIL', 'Reverse zones cannot contain A records') if $args{revrec} eq 'y'; 381 382 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 383 # or the intended parent domain for live records. 384 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 385 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 386 387 # Check IP is well-formed, and that it's a v4 address 388 # Fail on "compact" IPv4 variants, because they are not consistent and predictable. 389 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") 390 unless ${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/; 391 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv4 address") 392 unless $args{addr} && !$args{addr}->{isv6}; 393 # coerce IP/value to normalized form for storage 394 ${$args{val}} = $args{addr}->addr; 395 396 return ('OK','OK'); 397 } # done A record 398 399 # NS record 400 sub _validate_2 { 401 my $dbh = shift; 402 403 my %args = @_; 404 405 # Check that the target of the record is within the parent. 406 # Yes, host<->val are mixed up here; can't see a way to avoid it. :( 407 if ($args{defrec} eq 'n') { 408 # Check if IP/address/zone/"subzone" is within the parent 409 if ($args{revrec} eq 'y') { 410 my $tmpip = NetAddr::IP->new(${$args{val}}); 411 my $pname = revName($dbh,$args{id}); 412 return ('FAIL',"${$args{val}} not within $pname") 413 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip); 414 # Sub the returned thing for ZONE? This could get stupid if you have typos... 415 ${$args{val}} =~ s/ZONE/$tmpip->address/; 416 } else { 417 my $pname = domainName($dbh,$args{id}); 418 ${$args{host}} = $pname if ${$args{host}} !~ /\.$pname$/; 419 } 420 } else { 421 # Default reverse NS records should always refer to the implied parent 422 ${$args{host}} = 'DOMAIN' if $args{revrec} eq 'n'; 423 ${$args{val}} = 'ZONE' if $args{revrec} eq 'y'; 424 } 425 426 # Let this lie for now. Needs more magic. 427 # # Check IP is well-formed, and that it's a v4 address 428 # return ('FAIL',"A record must be a valid IPv4 address") 429 # unless $addr && !$addr->{isv6}; 430 # # coerce IP/value to normalized form for storage 431 # $$val = $addr->addr; 432 433 return ('OK','OK'); 434 } # done NS record 435 436 # CNAME record 437 sub _validate_5 { 438 my $dbh = shift; 439 440 my %args = @_; 441 442 # Not really true, but these are only useful for delegating smaller-than-/24 IP blocks. 443 # This is fundamentally a messy operation and should really just be taken care of by the 444 # export process, not manual maintenance of the necessary records. 445 return ('FAIL', 'Reverse zones cannot contain CNAME records') if $args{revrec} eq 'y'; 446 447 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 448 # or the intended parent domain for live records. 449 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 450 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 451 452 return ('OK','OK'); 453 } # done CNAME record 454 455 # SOA record 456 sub _validate_6 { 457 # Smart monkeys won't stick their fingers in here; we have 458 # separate dedicated routines to deal with SOA records. 459 return ('OK','OK'); 460 } # done SOA record 461 462 # PTR record 463 sub _validate_12 { 464 my $dbh = shift; 465 466 my %args = @_; 467 468 if ($args{revrec} eq 'y') { 469 if ($args{defrec} eq 'n') { 470 return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id})) 471 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr}); 472 ${$args{val}} = $args{addr}->addr; 473 } else { 474 if (${$args{val}} =~ /\./) { 475 # looks like a v4 or fragment 476 if (${$args{val}} =~ /^\d+\.\d+\.\d+\.\d+$/) { 477 # woo! a complete IP! validate it and normalize, or fail. 478 $args{addr} = NetAddr::IP->new(${$args{val}}) 479 or return ('FAIL', "IP/value looks like IPv4 but isn't valid"); 480 ${$args{val}} = $args{addr}->addr; 481 } else { 482 ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/; 483 } 484 } elsif (${$args{val}} =~ /[a-f:]/) { 485 # looks like a v6 or fragment 486 ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/; 487 if ($args{addr}) { 488 if ($args{addr}->addr =~ /^0/) { 489 ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/; 490 } else { 491 ${$args{val}} = $args{addr}->addr; 492 } 493 } 494 } else { 495 # bare number (probably). These could be v4 or v6, so we'll 496 # expand on these on creation of a reverse zone. 497 ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/; 498 } 499 ${$args{host}} =~ s/\.*$/\.$config{domain}/ if ${$args{host}} !~ /(?:$config{domain}|ADMINDOMAIN)$/; 500 } 501 502 # Multiple PTR records do NOT generally do what most people believe they do, 503 # and tend to fail in the most awkward way possible. Check and warn. 504 # We use $val instead of $addr->addr since we may be in a defrec, and may have eg "ZONE::42" or "ZONE.12" 505 506 my @checkvals = (${$args{val}}); 507 if (${$args{val}} =~ /,/) { 508 # push . and :: variants into checkvals if val has , 509 my $tmp; 510 ($tmp = ${$args{val}}) =~ s/,/./; 511 push @checkvals, $tmp; 512 ($tmp = ${$args{val}}) =~ s/,/::/; 513 push @checkvals, $tmp; 514 } 515 my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ?"); 516 foreach my $checkme (@checkvals) { 517 if ($args{update}) { 518 # Record update. There should usually be an existing PTR (the record being updated) 519 my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}). 520 " WHERE val = ?", undef, ($checkme)) }; 521 return ('WARN', "PTR record for $checkme already exists; adding another will probably not do what you want") 522 if @ptrs && (!grep /^$args{update}$/, @ptrs); 523 } else { 524 # New record. Always warn if a PTR exists 525 my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 526 " WHERE val = ?", undef, ($checkme)); 527 return ('WARN', "PTR record for $checkme already exists; adding another will probably not do what you want") 528 if $ptrcount; 529 } 530 } 531 532 } else { 533 # Not absolutely true but only useful if you hack things up for sub-/24 v4 reverse delegations 534 # Simpler to just create the reverse zone and grant access for the customer to edit it, and create direct 535 # PTR records on export 536 return ('FAIL',"Forward zones cannot contain PTR records"); 537 } 538 539 return ('OK','OK'); 540 } # done PTR record 541 542 # MX record 543 sub _validate_15 { 544 my $dbh = shift; 545 546 my %args = @_; 547 548 # Not absolutely true but WTF use is an MX record for a reverse zone? 549 return ('FAIL', 'Reverse zones cannot contain MX records') if $args{revrec} eq 'y'; 550 551 return ('FAIL', "Distance is required for MX records") unless defined(${$args{dist}}); 552 ${$args{dist}} =~ s/\s*//g; 553 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; 554 555 ${$args{fields}} = "distance,"; 556 push @{$args{vallist}}, ${$args{dist}}; 557 558 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 559 # or the intended parent domain for live records. 560 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 561 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 562 563 # hmm.. this might work. except possibly for something pointing to "deadbeef.ca". <g> 564 # if ($type == $reverse_typemap{NS} || $type == $reverse_typemap{MX} || $type == $reverse_typemap{SRV}) { 565 # if ($val =~ /^\s*[\da-f:.]+\s*$/) { 566 # return ('FAIL',"$val is not a valid IP address") if !$addr; 567 # } 568 # } 569 570 return ('OK','OK'); 571 } # done MX record 572 573 # TXT record 574 sub _validate_16 { 575 # Could arguably put a WARN return here on very long (>512) records 576 return ('OK','OK'); 577 } # done TXT record 578 579 # RP record 580 sub _validate_17 { 581 # Probably have to validate these some day 582 return ('OK','OK'); 583 } # done RP record 584 585 # AAAA record 586 sub _validate_28 { 587 my $dbh = shift; 588 589 my %args = @_; 590 591 return ('FAIL', 'Reverse zones cannot contain AAAA records') if $args{revrec} eq 'y'; 592 593 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 594 # or the intended parent domain for live records. 595 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 596 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 597 598 # Check IP is well-formed, and that it's a v6 address 599 return ('FAIL',"$typemap{${$args{rectype}}} record must be a valid IPv6 address") 600 unless $args{addr} && $args{addr}->{isv6}; 601 # coerce IP/value to normalized form for storage 602 ${$args{val}} = $args{addr}->addr; 603 604 return ('OK','OK'); 605 } # done AAAA record 606 607 # SRV record 608 sub _validate_33 { 609 my $dbh = shift; 610 611 my %args = @_; 612 613 # Not absolutely true but WTF use is an SRV record for a reverse zone? 614 return ('FAIL', 'Reverse zones cannot contain SRV records') if $args{revrec} eq 'y'; 615 616 return ('FAIL', "Distance is required for SRV records") unless defined(${$args{dist}}); 617 ${$args{dist}} =~ s/\s*//g; 618 return ('FAIL',"Distance is required, and must be numeric") unless ${$args{dist}} =~ /^\d+$/; 619 620 return ('FAIL',"SRV records must begin with _service._protocol [${$args{host}}]") 621 unless ${$args{host}} =~ /^_[A-Za-z]+\._[A-Za-z]+\.[a-zA-Z0-9-]+/; 622 return ('FAIL',"Port and weight are required for SRV records") 623 unless defined(${$args{weight}}) && defined(${$args{port}}); 624 ${$args{weight}} =~ s/\s*//g; 625 ${$args{port}} =~ s/\s*//g; 626 627 return ('FAIL',"Port and weight are required, and must be numeric") 628 unless ${$args{weight}} =~ /^\d+$/ && ${$args{port}} =~ /^\d+$/; 629 630 ${$args{fields}} = "distance,weight,port,"; 631 push @{$args{vallist}}, (${$args{dist}}, ${$args{weight}}, ${$args{port}}); 632 633 # Coerce all hostnames to end in ".DOMAIN" for group/default records, 634 # or the intended parent domain for live records. 635 my $pname = ($args{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$args{id})); 636 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 637 638 return ('OK','OK'); 639 } # done SRV record 640 641 # Now the custom types 642 643 # A+PTR record. With a very little bit of magic we can also use this sub to validate AAAA+PTR. Whee! 644 sub _validate_65280 { 645 my $dbh = shift; 646 647 my %args = @_; 648 649 my $code = 'OK'; 650 my $msg = 'OK'; 651 652 if ($args{defrec} eq 'n') { 653 # live record; revrec determines whether we validate the PTR or A component first. 654 655 if ($args{revrec} eq 'y') { 656 ($code,$msg) = _validate_12($dbh, %args); 657 return ($code,$msg) if $code eq 'FAIL'; 658 659 # Check if the reqested domain exists. If not, coerce the type down to PTR and warn. 660 if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) { 661 my $addmsg = "Record ".($args{update} ? 'updated' : 'added'). 662 " as PTR instead of $typemap{${$args{rectype}}}; domain not found for ${$args{host}}"; 663 $msg .= "\n$addmsg" if $code eq 'WARN'; 664 $msg = $addmsg if $code eq 'OK'; 665 ${$args{rectype}} = $reverse_typemap{PTR}; 666 return ('WARN', $msg); 667 } 668 669 # Add domain ID to field list and values 670 ${$args{fields}} .= "domain_id,"; 671 push @{$args{vallist}}, ${$args{domid}}; 672 673 } else { 674 ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280; 675 ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281; 676 return ($code,$msg) if $code eq 'FAIL'; 677 678 # Check if the requested reverse zone exists - note, an IP fragment won't 679 # work here since we don't *know* which parent to put it in. 680 # ${$args{val}} has been validated as a valid IP by now, in one of the above calls. 681 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?". 682 " ORDER BY masklen(revnet) DESC", undef, (${$args{val}})); 683 if (!$revid) { 684 $msg = "Record ".($args{update} ? 'updated' : 'added')." as ".(${$args{rectype}} == 65280 ? 'A' : 'AAAA'). 685 " instead of $typemap{${$args{rectype}}}; reverse zone not found for ${$args{val}}"; 686 ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA}); 687 return ('WARN', $msg); 688 } 689 690 # Check for duplicate PTRs. Note we don't have to play games with $code and $msg, because 691 # by definition there can't be duplicate PTRs if the reverse zone isn't managed here. 692 if ($args{update}) { 693 # Record update. There should usually be an existing PTR (the record being updated) 694 my @ptrs = @{ $dbh->selectcol_arrayref("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}). 695 " WHERE val = ?", undef, (${$args{val}})) }; 696 if (@ptrs && (!grep /^$args{update}$/, @ptrs)) { 697 $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want"; 698 $code = 'WARN'; 699 } 700 } else { 701 # New record. Always warn if a PTR exists 702 my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 703 " WHERE val = ?", undef, (${$args{val}})); 704 $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want" 705 if $ptrcount; 706 $code = 'WARN' if $ptrcount; 707 } 708 709 # my ($ptrcount) = $dbh->selectrow_array("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec}). 710 # " WHERE val = ?", undef, ${$args{val}}); 711 # if ($ptrcount) { 712 # my $curid = $dbh->selectrow_array("SELECT record_id FROM "._rectable($args{defrec},$args{revrec}). 713 # " WHERE val = ? 714 # $msg = "PTR record for ${$args{val}} already exists; adding another will probably not do what you want"; 715 # $code = 'WARN'; 716 # } 717 718 ${$args{fields}} .= "rdns_id,"; 719 push @{$args{vallist}}, $revid; 720 } 721 722 } else { # defrec eq 'y' 723 if ($args{revrec} eq 'y') { 724 ($code,$msg) = _validate_12($dbh, %args); 725 return ($code,$msg) if $code eq 'FAIL'; 726 if (${$args{rectype}} == 65280) { 727 return ('FAIL',"A+PTR record must be a valid IPv4 address or fragment") 728 if ${$args{val}} =~ /:/; 729 ${$args{val}} =~ s/^ZONE,/ZONE./; # Clean up after uncertain IP-fragment-type from _validate_12 730 } elsif (${$args{rectype}} == 65281) { 731 return ('FAIL',"AAAA+PTR record must be a valid IPv6 address or fragment") 732 if ${$args{val}} =~ /\./; 733 ${$args{val}} =~ s/^ZONE,/ZONE::/; # Clean up after uncertain IP-fragment-type from _validate_12 734 } 735 } else { 736 # This is easy. I also can't see a real use-case for A/AAAA+PTR in *all* forward 737 # domains, since you wouldn't be able to substitute both domain and reverse zone 738 # sanely, and you'd end up with guaranteed over-replicated PTR records that would 739 # confuse the hell out of pretty much anything that uses them. 740 ##fixme: make this a config flag? 741 return ('FAIL', "$typemap{${$args{rectype}}} records not allowed in default domains"); 742 } 743 } 744 745 return ($code, $msg); 746 } # done A+PTR record 747 748 # AAAA+PTR record 749 # A+PTR above has been magicked to handle AAAA+PTR as well. 750 sub _validate_65281 { 751 return _validate_65280(@_); 752 } # done AAAA+PTR record 753 754 # PTR template record 755 sub _validate_65282 { 756 my $dbh = shift; 757 758 my %args = @_; 759 760 # we're *this* >.< close to being able to just call _validate_12... unfortunately we can't, quite. 761 if ($args{revrec} eq 'y') { 762 if ($args{defrec} eq 'n') { 763 return ('FAIL', "Template block ${$args{val}} is not within ".revName($dbh, $args{id})) 764 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr}); 765 ##fixme: warn if $args{val} is not /31 or larger block? 766 ${$args{val}} = "$args{addr}"; 767 } else { 768 if (${$args{val}} =~ /\./) { 769 # looks like a v4 or fragment 770 if (${$args{val}} =~ m|^\d+\.\d+\.\d+\.\d+(?:/\d+)?$|) { 771 # woo! a complete IP! validate it and normalize, or fail. 772 $args{addr} = NetAddr::IP->new(${$args{val}}) 773 or return ('FAIL', "IP/value looks like IPv4 but isn't valid"); 774 ${$args{val}} = "$args{addr}"; 775 } else { 776 ${$args{val}} =~ s/^\.*/ZONE./ unless ${$args{val}} =~ /^ZONE/; 777 } 778 } elsif (${$args{val}} =~ /[a-f:]/) { 779 # looks like a v6 or fragment 780 ${$args{val}} =~ s/^:*/ZONE::/ if !$args{addr} && ${$args{val}} !~ /^ZONE/; 781 if ($args{addr}) { 782 if ($args{addr}->addr =~ /^0/) { 783 ${$args{val}} =~ s/^:*/ZONE::/ unless ${$args{val}} =~ /^ZONE/; 784 } else { 785 ${$args{val}} = "$args{addr}"; 786 } 787 } 788 } else { 789 # bare number (probably). These could be v4 or v6, so we'll 790 # expand on these on creation of a reverse zone. 791 ${$args{val}} = "ZONE,${$args{val}}" unless ${$args{val}} =~ /^ZONE/; 792 } 793 } 794 ##fixme: validate %-patterns? 795 796 # Unlike single PTR records, there is absolutely no way to sanely support multiple 797 # PTR templates for the same block, since they expect to expand to all the individual 798 # IPs on export. Nested templates should be supported though. 799 800 my @checkvals = (${$args{val}}); 801 if (${$args{val}} =~ /,/) { 802 # push . and :: variants into checkvals if val has , 803 my $tmp; 804 ($tmp = ${$args{val}}) =~ s/,/./; 805 push @checkvals, $tmp; 806 ($tmp = ${$args{val}}) =~ s/,/::/; 807 push @checkvals, $tmp; 808 } 809 ##fixme: this feels wrong still - need to restrict template pseudorecords to One Of Each 810 # Per Netblock such that they don't conflict on export 811 my $typeck; 812 # type 65282 -> ptr template -> look for any of 65282, 65283, 65284 813 $typeck = 'type=65283 OR type=65284' if ${$args{rectype}} == 65282; 814 # type 65283 -> a+ptr template -> v4 -> look for 65282 or 65283 815 $typeck = 'type=65283' if ${$args{rectype}} == 65282; 816 # type 65284 -> aaaa+ptr template -> v6 -> look for 65282 or 65284 817 $typeck = 'type=65284' if ${$args{rectype}} == 65282; 818 my $pcsth = $dbh->prepare("SELECT count(*) FROM "._rectable($args{defrec},$args{revrec})." WHERE val = ? ". 819 "AND (type=65282 OR $typeck)"); 820 foreach my $checkme (@checkvals) { 821 $pcsth->execute($checkme); 822 my ($rc) = $pcsth->fetchrow_array; 823 return ('FAIL', "Only one template pseudorecord may exist for a given IP block") if $rc; 824 } 825 826 } else { 827 return ('FAIL', "Forward zones cannot contain PTR records"); 828 } 829 830 return ('OK','OK'); 831 } # done PTR template record 832 833 # A+PTR template record 834 sub _validate_65283 { 835 my $dbh = shift; 836 837 my %args = @_; 838 839 my ($code,$msg) = ('OK','OK'); 840 841 ##fixme: need to fiddle things since A+PTR templates are acceptable in live 842 # forward zones but not default records 843 if ($args{defrec} eq 'n') { 844 if ($args{revrec} eq 'n') { 845 ($code,$msg) = _validate_1($dbh, %args) if ${$args{rectype}} == 65280; 846 ($code,$msg) = _validate_28($dbh, %args) if ${$args{rectype}} == 65281; 847 return ($code,$msg) if $code eq 'FAIL'; 848 849 # Check if the requested reverse zone exists - note, an IP fragment won't 850 # work here since we don't *know* which parent to put it in. 851 # ${$args{val}} has been validated as a valid IP by now, in one of the above calls. 852 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?". 853 " ORDER BY masklen(revnet) DESC", undef, (${$args{val}})); 854 # Fail if no match; we can't coerce a PTR-template type down to not include the PTR bit currently. 855 if (!$revid) { 856 $msg = "Can't ".($args{update} ? 'update' : 'add')." ${$args{host}}/${$args{val}} as ". 857 "$typemap{${$args{rectype}}}: reverse zone not found for ${$args{val}}"; 858 ##fixme: add A template, AAAA template types? 859 # ${$args{rectype}} = (${$args{rectype}} == 65280 ? $reverse_typemap{A} : $reverse_typemap{AAAA}); 860 return ('FAIL', $msg); 861 } 862 863 # Add reverse zone ID to field list and values 864 ${$args{fields}} .= "rdns_id,"; 865 push @{$args{vallist}}, $revid; 866 867 } else { 868 return ('FAIL', "IP or IP fragment ${$args{val}} is not within ".revName($dbh, $args{id})) 869 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$args{addr}); 870 ${$args{val}} = "$args{addr}"; 871 872 if (!(${$args{domid}} = _hostparent($dbh, ${$args{host}}))) { 873 my $addmsg = "Record ".($args{update} ? 'updated' : 'added'). 874 " as PTR template instead of $typemap{${$args{rectype}}}; domain not found for ${$args{host}}"; 875 $msg .= "\n$addmsg" if $code eq 'WARN'; 876 $msg = $addmsg if $code eq 'OK'; 877 ${$args{rectype}} = 65282; 878 return ('WARN', $msg); 879 } 880 881 # Add domain ID to field list and values 882 ${$args{fields}} .= "domain_id,"; 883 push @{$args{vallist}}, ${$args{domid}}; 884 } 885 886 } else { 887 my ($code,$msg) = _validate_65282($dbh, %args); 888 return ($code, $msg) if $code eq 'FAIL'; 889 # get domain, check against ${$args{name}} 890 } 891 892 return ('OK','OK'); 893 } # done AAAA+PTR template record 894 895 # AAAA+PTR template record 896 sub _validate_65284 { 897 return ('OK','OK'); 898 } # done AAAA+PTR template record 899 900 # Delegation record 901 # This is essentially a specialized clone of the NS record, primarily useful 902 # for delegating IPv4 sub-/24 reverse blocks 903 sub _validate_65285 { 904 my $dbh = shift; 905 906 my %args = @_; 907 908 # Almost, but not quite, identical to NS record validation. 909 910 # Check that the target of the record is within the parent. 911 # Yes, host<->val are mixed up here; can't see a way to avoid it. :( 912 if ($args{defrec} eq 'n') { 913 # Check if IP/address/zone/"subzone" is within the parent 914 if ($args{revrec} eq 'y') { 915 my $tmpip = NetAddr::IP->new(${$args{val}}); 916 my $pname = revName($dbh,$args{id}); 917 return ('FAIL',"${$args{val}} not within $pname") 918 unless _ipparent($dbh, $args{defrec}, $args{revrec}, $args{val}, $args{id}, \$tmpip); 919 # Normalize 920 ${$args{val}} = "$tmpip"; 921 } else { 922 my $pname = domainName($dbh,$args{id}); 923 ${$args{host}} =~ s/\.*$/\.$pname/ if ${$args{host}} !~ /$pname$/; 924 } 925 } else { 926 return ('FAIL',"Delegation records are not permitted in default record sets"); 927 } 928 return ('OK','OK'); 929 } 930 931 932 ## 933 ## Record data substitution subs 934 ## 935 936 # Replace ZONE in hostname, or create (most of) the actual proper zone name 937 sub _ZONE { 938 my $zone = shift; 939 my $string = shift; 940 my $fr = shift || 'f'; # flag for forward/reverse order? nb: ignored for IP 941 my $sep = shift || '-'; # Separator character - unlikely we'll ever need more than . or - 942 943 my $prefix; 944 945 $string =~ s/,/./ if !$zone->{isv6}; 946 $string =~ s/,/::/ if $zone->{isv6}; 947 948 # Subbing ZONE in the host. We need to properly ID the netblock range 949 # The subbed text should have "network IP with trailing zeros stripped" for 950 # blocks lined up on octet (for v4) or hex-quad (for v6) boundaries 951 # For blocks that do NOT line up on these boundaries, we take the most 952 # significant octet or 16-bit chunk of the "broadcast" IP and append it 953 # after a double-dash 954 # ie: 955 # 8.0.0.0/6 -> 8.0.0.0 -> 11.255.255.255; sub should be 8--11 956 # 10.0.0.0/12 -> 10.0.0.0 -> 10.0.0.0 -> 10.15.255.255; sub should be 10-0--15 957 # 192.168.4.0/22 -> 192.168.4.0 -> 192.168.7.255; sub should be 192-168-4--7 958 # 192.168.0.8/29 -> 192.168.0.8 -> 192.168.0.15; sub should be 192-168-0-8--15 959 # Similar for v6 960 961 if (!$zone->{isv6}) { # IPv4 962 963 $prefix = $zone->network->addr; # Just In Case someone managed to slip in 964 # a funky subnet that had host bits set. 965 my $bc = $zone->broadcast->addr; 966 967 if ($zone->masklen > 24) { 968 $bc =~ s/^\d+\.\d+\.\d+\.//; 969 } elsif ($zone->masklen > 16) { 970 $prefix =~ s/\.0$//; 971 $bc =~ s/^\d+\.\d+\.//; 972 } elsif ($zone->masklen > 8) { 973 $bc =~ s/^\d+\.//; 974 $prefix =~ s/\.0\.0$//; 975 } else { 976 $prefix =~ s/\.0\.0\.0$//; 977 } 978 if ($zone->masklen % 8) { 979 $bc =~ s/(\.255)+$//; 980 $prefix .= "--$bc"; #"--".zone->masklen; # use range or mask length? 981 } 982 if ($fr eq 'f') { 983 $prefix =~ s/\.+/$sep/g; 984 } else { 985 $prefix = join($sep, reverse(split(/\./, $prefix))); 986 } 987 988 } else { # IPv6 989 990 if ($fr eq 'f') { 991 992 $prefix = $zone->network->addr; # Just In Case someone managed to slip in 993 # a funky subnet that had host bits set. 994 my $bc = $zone->broadcast->addr; 995 if (($zone->masklen % 16) != 0) { 996 # Strip trailing :0 off $prefix, and :ffff off the broadcast IP 997 for (my $i=0; $i<(7-int($zone->masklen / 16)); $i++) { 998 $prefix =~ s/:0$//; 999 $bc =~ s/:ffff$//; 1000 } 1001 # Strip the leading 16-bit chunks off the front of the broadcast IP 1002 $bc =~ s/^([a-f0-9]+:)+//; 1003 # Append the remaining 16-bit chunk to the prefix after "--" 1004 $prefix .= "--$bc"; 1005 } else { 1006 # Strip off :0 from the end until we reach the netblock length. 1007 for (my $i=0; $i<(8-$zone->masklen / 16); $i++) { 1008 $prefix =~ s/:0$//; 1009 } 1010 } 1011 # Actually deal with the separator 1012 $prefix =~ s/:/$sep/g; 1013 1014 } else { # $fr eq 'f' 1015 1016 $prefix = $zone->network->full; # Just In Case someone managed to slip in 1017 # a funky subnet that had host bits set. 1018 my $bc = $zone->broadcast->full; 1019 $prefix =~ s/://g; # clean these out since they're not spaced right for this case 1020 $bc =~ s/://g; 1021 # Strip trailing 0 off $prefix, and f off the broadcast IP, to match the mask length 1022 for (my $i=0; $i<(31-int($zone->masklen / 4)); $i++) { 1023 $prefix =~ s/0$//; 1024 $bc =~ s/f$//; 1025 } 1026 # Split and reverse the order of the nibbles in the network/broadcast IPs 1027 # trim another 0 for nibble-aligned blocks first, but only if we really have a block, not an IP 1028 $prefix =~ s/0$// if $zone->masklen % 4 == 0 && $zone->masklen != 128; 1029 my @nbits = reverse split //, $prefix; 1030 my @bbits = reverse split //, $bc; 1031 # Handle the sub-nibble case. Eww. I feel dirty supporting this... 1032 $nbits[0] = "$nbits[0]-$bbits[0]" if ($zone->masklen % 4) != 0; 1033 # Glue it back together 1034 $prefix = join($sep, @nbits); 1035 1036 } # $fr ne 'f' 1037 1038 } # $zone->{isv6} 1039 1040 # Do the substitution, finally 1041 $string =~ s/ZONE/$prefix/; 1042 $string =~ s/--/-/ if $sep ne '-'; # - as separator needs extra help for sub-octet v4 netblocks 1043 return $string; 1044 } # done _ZONE() 1045 1046 # Not quite a substitution sub, but placed here as it's basically the inverse of above; 1047 # given the .arpa zone name, return the CIDR netblock the zone is for. 1048 # Supports v4 non-octet/non-classful netblocks as per the method outlined in the Grasshopper Book (2nd Ed p217-218) 1049 # Does NOT support non-quad v6 netblocks via the same scheme; it shouldn't ever be necessary. 1050 # Takes a nominal .arpa zone name, returns a success code and NetAddr::IP, or a fail code and message 1051 sub _zone2cidr { 1052 my $zone = shift; 1053 1054 my $cidr; 1055 my $tmpcidr; 1056 my $warnmsg = ''; 1057 1058 if ($zone =~ /\.in-addr\.arpa\.?$/) { 1059 # v4 revzone, formal zone name type 1060 my $tmpzone = $zone; 1061 $tmpzone =~ s/\.in-addr\.arpa\.?//; 1062 return ('FAIL', "Non-numerics in apparent IPv4 reverse zone name") if $tmpzone !~ /^(?:\d+-)?[\d\.]+$/; 1063 1064 # Snag the octet pieces 1065 my @octs = split /\./, $tmpzone; 1066 1067 # Map result of a range manipulation to a mask length change. Cheaper than finding the 2-root of $octets[0]+1. 1068 # Note we will not support /31 blocks, mostly due to issues telling "24-31" -> .24/29 apart from 1069 # "24-31" -> .24/31", with a litte bit of "/31 is icky". 1070 my %maskmap = ( 3 => 2, 7 => 3, 15 => 4, 31 => 5, 63 => 6, 127 => 7, 1071 30 => 2, 29 => 3, 28 => 4, 27 => 5, 26 => 6, 25 => 7 1072 ); 1073 1074 # Handle "range" blocks, eg, 80-83.168.192.in-addr.arpa (192.168.80.0/22) 1075 # Need to take the size of the range to offset the basic octet-based mask length, 1076 # and make sure the first number in the range gets used as the network address for the block 1077 # Alternate form: The second number is actually the real netmask, not the end of the range. 1078 my $masklen = 0; 1079 if ($octs[0] =~ /^((\d+)-(\d+))$/) { # take the range... 1080 if (24 < $3 && $3 < 31) { 1081 # we have a real netmask 1082 $masklen = -$maskmap{$3}; 1083 } else { 1084 # we have a range. NB: only real CIDR ranges are supported 1085 $masklen -= $maskmap{-(eval $1)}; # find the mask base... 1086 } 1087 $octs[0] = $2; # set the base octet of the range... 1088 } 1089 @octs = reverse @octs; # We can reverse the octet pieces now that we've extracted and munged any ranges 1090 1091 # arguably we should only allow sub-octet range/mask in-addr.arpa 1092 # specifications in the least significant octet, but the code is 1093 # simpler if we deal with sub-octet delegations at any level. 1094 1095 # Now we find the "true" mask with the aid of the "base" calculated above 1096 if ($#octs == 0) { 1097 $masklen += 8; 1098 $tmpcidr = "$octs[0].0.0.0/$masklen"; # really hope we don't see one of these very often. 1099 } elsif ($#octs == 1) { 1100 $masklen += 16; 1101 $tmpcidr = "$octs[0].$octs[1].0.0/$masklen"; 1102 } elsif ($#octs == 2) { 1103 $masklen += 24; 1104 $tmpcidr = "$octs[0].$octs[1].$octs[2].0/$masklen"; 1105 } else { 1106 $masklen += 32; 1107 $tmpcidr = "$octs[0].$octs[1].$octs[2].$octs[3]/$masklen"; 1108 } 1109 1110 } elsif ($zone =~ /\.ip6\.arpa$/) { 1111 # v6 revzone, formal zone name type 1112 my $tmpzone = $zone; 1113 $tmpzone =~ s/\.ip6\.arpa\.?//; 1114 ##fixme: if-n-when we decide we can support sub-nibble v6 zone names, we'll need to change this segment 1115 return ('FAIL', "Non-hexadecimals in apparent IPv6 reverse zone name") if $tmpzone !~ /^[a-fA-F\d\.]+$/; 1116 my @quads = reverse(split(/\./, $tmpzone)); 1117 $warnmsg .= "Apparent sub-/64 IPv6 reverse zone\n" if $#quads > 15; 1118 my $nc; 1119 foreach (@quads) { 1120 $tmpcidr .= $_; 1121 $tmpcidr .= ":" if ++$nc % 4 == 0; 1122 } 1123 my $nq = 1 if $nc % 4 != 0; 1124 my $mask = $nc * 4; # need to do this here because we probably increment it below 1125 while ($nc++ % 4 != 0) { 1126 $tmpcidr .= "0"; 1127 } 1128 $tmpcidr .= ($nq ? '::' : ':')."/$mask"; 1129 } 1130 1131 # Just to be sure, use NetAddr::IP to validate. Saves a lot of nasty regex watching for valid octet values. 1132 return ('FAIL', "Invalid zone $zone (apparent netblock $tmpcidr)") 1133 unless $cidr = NetAddr::IP->new($tmpcidr); 1134 1135 if ($warnmsg) { 1136 $errstr = $warnmsg; 1137 return ('WARN', $cidr); 1138 } 1139 return ('OK', $cidr); 1140 } # done _zone2cidr() 1141 1142 # Record template %-parameter expansion, IPv4. Note that IPv6 doesn't 1143 # really have a sane way to handle this type of expansion at the moment 1144 # due to the size of the address space. 1145 # Takes a reference to a template string to be expanded, and an IP to use in the replacement. 1146 sub _template4_expand { 1147 my $tmpl = shift; 1148 my $ip = shift; 1149 1150 my @ipparts = split /\./, $ip; 1151 my @iphex; 1152 my @ippad; 1153 for (@ipparts) { 1154 push @iphex, sprintf("%x", $_); 1155 push @ippad, sprintf("%u.3", $_); 1156 } 1157 1158 # IP substitutions in template records: 1159 #major patterns: 1160 #dashed IP, forward and reverse 1161 #dotted IP, forward and reverse (even if forward is... dumb) 1162 # -> %r for reverse, %i for forward, leading - or . to indicate separator, defaults to - 1163 # %r or %-r => %4d-%3d-%2d-%1d 1164 # %.r => %4d.%3d.%2d.%1d 1165 # %i or %-i => %1d-%2d-%3d-%4d 1166 # %.i => %1d.%2d.%3d.%4d 1167 $$tmpl =~ s/\%r/\%4d-\%3d-\%2d-\%1d/g; 1168 $$tmpl =~ s/\%([-.])r/\%4d$1\%3d$1\%2d$1\%1d/g; 1169 $$tmpl =~ s/\%i/\%1d-\%2d-\%3d-\%4d/g; 1170 $$tmpl =~ s/\%([-.])i/\%1d$1\%2d$1\%3d$1\%4d/g; 1171 1172 #hex-coded IP 1173 # %h 1174 $$tmpl =~ s/\%h/$iphex[0]$iphex[1]$iphex[2]$iphex[3]/g; 1175 1176 #IP as decimal-coded 32-bit value 1177 # %d 1178 my $iptmp = $ipparts[0]*256*256*256 + $ipparts[1]*256*256 + $ipparts[2]*256 + $ipparts[3]; 1179 $$tmpl =~ s/\%d/$iptmp/g; 1180 1181 #minor patterns (per-octet) 1182 # %[1234][dh0] 1183 #octet 1184 #hex-coded octet 1185 #0-padded octet 1186 $$tmpl =~ s/\%([1234])d/$ipparts[$1-1]/g; 1187 $$tmpl =~ s/\%([1234])h/$iphex[$1-1]/g; 1188 $$tmpl =~ s/\%([1234])h/$ippad[$1-1]/g; 1189 } # _template4_expand() 1190 1191 1192 ## 1193 ## Initialization and cleanup subs 1194 ## 1195 1196 1197 ## DNSDB::loadConfig() 1198 # Load the minimum required initial state (DB connect info) from a config file 1199 # Load misc other bits while we're at it. 1200 # Takes an optional hash that may contain: 1201 # - basename and config path to look for 1202 # - RPC flag (saves parsing the more complex RPC bits if not needed) 1203 # Populates the %config and %def hashes 1204 sub loadConfig { 1205 my %args = @_; 1206 $args{basename} = '' if !$args{basename}; 1207 $args{rpcflag} = '' if !$args{rpcflag}; 1208 ##fixme $args{basename} isn't doing what I think I thought I was trying to do. 1209 1210 my $deferr = ''; # place to put error from default config file in case we can't find either one 1211 1212 my $configroot = "/etc/dnsdb"; ##CFG_LEAF## 1213 $configroot = '' if $args{basename} =~ m|^/|; 1214 $args{basename} .= ".conf" if $args{basename} !~ /\.conf$/; 1215 my $defconfig = "$configroot/dnsdb.conf"; 1216 my $siteconfig = "$configroot/$args{basename}"; 1217 1218 # System defaults 1219 __cfgload("$defconfig", $args{rpcflag}) or $deferr = $errstr; 1220 1221 # Per-site-ish settings. 1222 if ($args{basename} ne '.conf') { 1223 unless (__cfgload("$siteconfig"), $args{rpcflag}) { 1224 $errstr = ($deferr ? "Error opening default config file $defconfig: $deferr\n" : ''). 1225 "Error opening site config file $siteconfig"; 1226 return; 1227 } 1228 } 1229 1230 # Munge log_failures. 1231 if ($config{log_failures} ne '1' && $config{log_failures} ne '0') { 1232 # true/false, on/off, yes/no all valid. 1233 if ($config{log_failures} =~ /^(?:true|false|on|off|yes|no)$/) { 1234 if ($config{log_failures} =~ /(?:true|on|yes)/) { 1235 $config{log_failures} = 1; 1236 } else { 1237 $config{log_failures} = 0; 1238 } 1239 } else { 1240 $errstr = "Bad log_failures setting $config{log_failures}"; 1241 $config{log_failures} = 1; 1242 # Bad setting shouldn't be fatal. 1243 # return 2; 1244 } 1245 } 1246 1247 # All good, clear the error and go home. 1248 $errstr = ''; 1249 return 1; 1250 } # end loadConfig() 1251 1252 1253 ## DNSDB::__cfgload() 1254 # Private sub to parse a config file and load it into %config 1255 # Takes a file handle on an open config file 1256 sub __cfgload { 1257 $errstr = ''; 1258 my $cfgfile = shift; 1259 my $rpcflag = shift; 1260 1261 if (open CFG, "<$cfgfile") { 1262 while (<CFG>) { 1263 chomp; 1264 s/^\s*//; 1265 next if /^#/; 1266 next if /^$/; 1267 # hmm. more complex bits in this file might require [heading] headers, maybe? 1268 # $mode = $1 if /^\[(a-z)+]/; 1269 # DB connect info 1270 $config{dbname} = $1 if /^dbname\s*=\s*([a-z0-9_.-]+)/i; 1271 $config{dbuser} = $1 if /^dbuser\s*=\s*([a-z0-9_.-]+)/i; 1272 $config{dbpass} = $1 if /^dbpass\s*=\s*([a-z0-9_.-]+)/i; 1273 $config{dbhost} = $1 if /^dbhost\s*=\s*([a-z0-9_.-]+)/i; 1274 # SOA defaults 1275 $def{contact} = $1 if /^contact\s*=\s*([a-z0-9_.-]+)/i; 1276 $def{prins} = $1 if /^prins\s*=\s*([a-z0-9_.-]+)/i; 1277 $def{soattl} = $1 if /^soattl\s*=\s*(\d+)/i; 1278 $def{refresh} = $1 if /^refresh\s*=\s*(\d+)/i; 1279 $def{retry} = $1 if /^retry\s*=\s*(\d+)/i; 1280 $def{expire} = $1 if /^expire\s*=\s*(\d+)/i; 1281 $def{minttl} = $1 if /^minttl\s*=\s*(\d+)/i; 1282 $def{ttl} = $1 if /^ttl\s*=\s*(\d+)/i; 1283 # Mail settings 1284 $config{mailhost} = $1 if /^mailhost\s*=\s*([a-z0-9_.-]+)/i; 1285 $config{mailnotify} = $1 if /^mailnotify\s*=\s*([a-z0-9_.\@-]+)/i; 1286 $config{mailsender} = $1 if /^mailsender\s*=\s*([a-z0-9_.\@-]+)/i; 1287 $config{mailname} = $1 if /^mailname\s*=\s*([a-z0-9\s_.-]+)/i; 1288 $config{orgname} = $1 if /^orgname\s*=\s*([a-z0-9\s_.,'-]+)/i; 1289 $config{domain} = $1 if /^domain\s*=\s*([a-z0-9_.-]+)/i; 1290 # session - note this is fed directly to CGI::Session 1291 $config{timeout} = $1 if /^[tT][iI][mM][eE][oO][uU][tT]\s*=\s*(\d+[smhdwMy]?)/; 1292 $config{sessiondir} = $1 if m{^sessiondir\s*=\s*([a-z0-9/_.-]+)}i; 1293 # misc 1294 $config{log_failures} = $1 if /^log_failures\s*=\s*([a-z01]+)/i; 1295 $config{perpage} = $1 if /^perpage\s*=\s*(\d+)/i; 1296 $config{exportcache} = $1 if m{^exportcache\s*=\s*([a-z0-9/_.-]+)}i; 1297 # RPC options 1298 if ($rpcflag && /^rpc/) { 1299 if (my ($tmp) = /^rpc_iplist\s*=\s*(.+)/i) { 1300 my @ips = split /[,\s]+/, $tmp; 1301 my $rpcsys = shift @ips; 1302 push @{$config{rpcacl}{$rpcsys}}, @ips; 1303 } 1304 } 1305 } 1306 close CFG; 1307 } else { 1308 $errstr = $!; 1309 return; 1310 } 1311 return 1; 1312 } # end __cfgload() 1313 1314 1315 ## DNSDB::connectDB() 1316 # Creates connection to DNS database. 1317 # Requires the database name, username, and password. 1318 # Returns a handle to the db. 1319 # Set up for a PostgreSQL db; could be any transactional DBMS with the 1320 # right changes. 1321 sub connectDB { 1322 $errstr = ''; 1323 my $dbname = shift; 1324 my $user = shift; 1325 my $pass = shift; 1326 my $dbh; 1327 my $DSN = "DBI:Pg:dbname=$dbname"; 1328 1329 my $host = shift; 1330 $DSN .= ";host=$host" if $host; 1331 1332 # Note that we want to autocommit by default, and we will turn it off locally as necessary. 1333 # We may not want to print gobbledygook errors; YMMV. Have to ponder that further. 1334 $dbh = DBI->connect($DSN, $user, $pass, { 1335 AutoCommit => 1, 1336 PrintError => 0 1337 }) 1338 or return (undef, $DBI::errstr) if(!$dbh); 1339 1340 ##fixme: initialize the DB if we can't find the table (since, by definition, there's 1341 # nothing there if we can't select from it...) 1342 my $tblsth = $dbh->prepare("SELECT count(*) FROM pg_catalog.pg_class WHERE relkind='r' AND relname=?"); 1343 my ($tblcount) = $dbh->selectrow_array($tblsth, undef, ('misc')); 1344 return (undef,$DBI::errstr) if $dbh->err; 1345 1346 #if ($tblcount == 0) { 1347 # # create tables one at a time, checking for each. 1348 # return (undef, "check table misc missing"); 1349 #} 1350 1351 1352 # Return here if we can't select. 1353 # This should retrieve the dbversion key. 1354 my $sth = $dbh->prepare("SELECT key,value FROM misc WHERE misc_id=1"); 1355 $sth->execute(); 1356 return (undef,$DBI::errstr) if ($sth->err); 1357 1358 ##fixme: do stuff to the DB on version mismatch 1359 # x.y series should upgrade on $DNSDB::VERSION > misc(key=>version) 1360 # DB should be downward-compatible; column defaults should give sane (if possibly 1361 # useless-and-needs-help) values in columns an older software stack doesn't know about. 1362 1363 # See if the select returned anything (or null data). This should 1364 # succeed if the select executed, but... 1365 $sth->fetchrow(); 1366 return (undef,$DBI::errstr) if ($sth->err); 1367 1368 $sth->finish; 1369 1370 # If we get here, we should be OK. 1371 return ($dbh,"DB connection OK"); 1372 } # end connectDB 1373 1374 1375 ## DNSDB::finish() 1376 # Cleans up after database handles and so on. 1377 # Requires a database handle 1378 sub finish { 1379 my $dbh = $_[0]; 1380 $dbh->disconnect; 1381 } # end finish 1382 1383 1384 ## DNSDB::initGlobals() 1385 # Initialize global variables 1386 # NB: this does NOT include web-specific session variables! 1387 # Requires a database handle 1388 sub initGlobals { 1389 my $dbh = shift; 1390 1391 # load record types from database 1392 my $sth = $dbh->prepare("SELECT val,name,stdflag FROM rectypes"); 1393 $sth->execute; 1394 while (my ($recval,$recname,$stdflag) = $sth->fetchrow_array()) { 1395 $typemap{$recval} = $recname; 1396 $reverse_typemap{$recname} = $recval; 1397 # now we fill the record validation function hash 1398 if ($stdflag < 5) { 1399 my $fn = "_validate_$recval"; 1400 $validators{$recval} = \&$fn; 1401 } else { 1402 my $fn = "sub { return ('FAIL','Type $recval ($recname) not supported'); }"; 1403 $validators{$recval} = eval $fn; 1404 } 1405 } 1406 } # end initGlobals 1407 1408 1409 ## DNSDB::initRPC() 1410 # Takes a database handle, remote username, and remote fullname. 1411 # Sets up the RPC logging-pseudouser if needed. 1412 # Sets the %userdata hash for logging. 1413 # Returns undef on failure 1414 sub initRPC { 1415 my $dbh = shift; 1416 my %args = @_; 1417 1418 return if !$args{username}; 1419 return if !$args{fullname}; 1420 1421 $args{username} = "$args{username}/$args{rpcsys}"; 1422 1423 my $tmpuser = $dbh->selectrow_hashref("SELECT username,user_id AS userid,group_id,firstname,lastname,status". 1424 " FROM users WHERE username=?", undef, ($args{username}) ); 1425 if (!$tmpuser) { 1426 $dbh->do("INSERT INTO users (username,password,firstname,type) VALUES (?,'RPC',?,'R')", undef, 1427 ($args{username}, $args{fullname}) ); 1428 $tmpuser = $dbh->selectrow_hashref("SELECT username,user_id AS userid,group_id,firstname,lastname,status". 1429 " FROM users WHERE username=?", undef, ($args{username}) ); 1430 } 1431 %userdata = %{$tmpuser}; 1432 $userdata{lastname} = '' if !$userdata{lastname}; 1433 $userdata{fullname} = "$userdata{firstname} $userdata{lastname} ($args{rpcsys})"; 1434 return 1 if $tmpuser; 1435 } # end initRPC() 1436 1437 1438 ## DNSDB::login() 1439 # Takes a database handle, username and password 1440 # Returns a userdata hash (UID, GID, username, fullname parts) if username exists, 1441 # password matches the one on file, and account is not disabled 1442 # Returns undef otherwise 1443 sub login { 1444 my $dbh = shift; 1445 my $user = shift; 1446 my $pass = shift; 1447 1448 my $userinfo = $dbh->selectrow_hashref("SELECT user_id,group_id,password,firstname,lastname,status". 1449 " FROM users WHERE username=?", 1450 undef, ($user) ); 1451 return if !$userinfo; 1452 return if !$userinfo->{status}; 1453 1454 if ($userinfo->{password} =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) { 1455 # native passwords (crypt-md5) 1456 return if $userinfo->{password} ne unix_md5_crypt($pass,$1); 1457 } elsif ($userinfo->{password} =~ /^[0-9a-f]{32}$/) { 1458 # VegaDNS import (hex-coded MD5) 1459 return if $userinfo->{password} ne md5_hex($pass); 1460 } else { 1461 # plaintext (convenient now and then) 1462 return if $userinfo->{password} ne $pass; 1463 } 1464 1465 return $userinfo; 1466 } # end login() 1467 1468 1469 ## DNSDB::initActionLog() 1470 # Set up action logging. Takes a database handle and user ID 1471 # Sets some internal globals and Does The Right Thing to set up a logging channel. 1472 # This sets up _log() to spew out log entries to the defined channel without worrying 1473 # about having to open a file or a syslog channel 1474 ##fixme Need to call _initActionLog_blah() for various logging channels, configured 1475 # via dnsdb.conf, in $config{log_channel} or something 1476 # See https://secure.deepnet.cx/trac/dnsadmin/ticket/21 1477 sub initActionLog { 1478 my $dbh = shift; 1479 my $uid = shift; 1480 1481 return if !$uid; 1482 1483 # snag user info for logging. there's got to be a way to not have to pass this back 1484 # and forth from a caller, but web usage means no persistence we can rely on from 1485 # the server side. 1486 my ($username,$fullname) = $dbh->selectrow_array("SELECT username, firstname || ' ' || lastname". 1487 " FROM users WHERE user_id=?", undef, ($uid)); 1488 ##fixme: errors are unpossible! 1489 1490 $userdata{username} = $username; 1491 $userdata{userid} = $uid; 1492 $userdata{fullname} = $fullname; 1493 1494 # convert to real check once we have other logging channels 1495 # if ($config{log_channel} eq 'sql') { 1496 # Open Log, Sez Me! 1497 # } 1498 1499 } # end initActionLog 1500 1501 1502 ## DNSDB::initPermissions() 1503 # Set up permissions global 1504 # Takes database handle and UID 1505 sub initPermissions { 1506 my $dbh = shift; 1507 my $uid = shift; 1508 1509 # %permissions = $(getPermissions($dbh,'user',$uid)); 1510 getPermissions($dbh, 'user', $uid, \%permissions); 1511 1512 } # end initPermissions() 1513 1514 1515 ## DNSDB::getPermissions() 1516 # Get permissions from DB 1517 # Requires DB handle, group or user flag, ID, and hashref. 1518 sub getPermissions { 1519 my $dbh = shift; 1520 my $type = shift; 1521 my $id = shift; 1522 my $hash = shift; 1523 1524 my $sql = qq( 1525 SELECT 1526 p.admin,p.self_edit, 1527 p.group_create,p.group_edit,p.group_delete, 1528 p.user_create,p.user_edit,p.user_delete, 1529 p.domain_create,p.domain_edit,p.domain_delete, 1530 p.record_create,p.record_edit,p.record_delete,p.record_locchg, 1531 p.location_create,p.location_edit,p.location_delete,p.location_view 1532 FROM permissions p 1533 ); 1534 if ($type eq 'group') { 1535 $sql .= qq( 1536 JOIN groups g ON g.permission_id=p.permission_id 1537 WHERE g.group_id=? 1538 ); 1539 } else { 1540 $sql .= qq( 1541 JOIN users u ON u.permission_id=p.permission_id 1542 WHERE u.user_id=? 1543 ); 1544 } 1545 1546 my $sth = $dbh->prepare($sql); 1547 1548 $sth->execute($id) or die "argh: ".$sth->errstr; 1549 1550 # my $permref = $sth->fetchrow_hashref; 1551 # return $permref; 1552 # $hash = $permref; 1553 # Eww. Need to learn how to forcibly drop a hashref onto an existing hash. 1554 ($hash->{admin},$hash->{self_edit}, 1555 $hash->{group_create},$hash->{group_edit},$hash->{group_delete}, 1556 $hash->{user_create},$hash->{user_edit},$hash->{user_delete}, 1557 $hash->{domain_create},$hash->{domain_edit},$hash->{domain_delete}, 1558 $hash->{record_create},$hash->{record_edit},$hash->{record_delete},$hash->{record_locchg}, 1559 $hash->{location_create},$hash->{location_edit},$hash->{location_delete},$hash->{location_view} 1560 ) = $sth->fetchrow_array; 1561 1562 } # end getPermissions() 1563 1564 1565 ## DNSDB::changePermissions() 1566 # Update an ACL entry 1567 # Takes a db handle, type, owner-id, and hashref for the changed permissions. 1568 sub changePermissions { 1569 my $dbh = shift; 1570 my $type = shift; 1571 my $id = shift; 1572 my $newperms = shift; 1573 my $inherit = shift || 0; 1574 1575 my $resultmsg = ''; 1576 1577 # see if we're switching from inherited to custom. for bonus points, 1578 # snag the permid and parent permid anyway, since we'll need the permid 1579 # to set/alter custom perms, and both if we're switching from custom to 1580 # inherited. 1581 my $sth = $dbh->prepare("SELECT (u.permission_id=g.permission_id) AS was_inherited,u.permission_id,g.permission_id,". 1582 ($type eq 'user' ? 'u.group_id,u.username' : 'u.parent_group_id,u.group_name'). 1583 " FROM ".($type eq 'user' ? 'users' : 'groups')." u ". 1584 " JOIN groups g ON u.".($type eq 'user' ? '' : 'parent_')."group_id=g.group_id ". 1585 " WHERE u.".($type eq 'user' ? 'user' : 'group')."_id=?"); 1586 $sth->execute($id); 1587 1588 my ($wasinherited,$permid,$parpermid,$parid,$name) = $sth->fetchrow_array; 1589 1590 # hack phtoui 1591 # group id 1 is "special" in that it's it's own parent (err... possibly.) 1592 # may make its parent id 0 which doesn't exist, and as a bonus is Perl-false. 1593 $wasinherited = 0 if ($type eq 'group' && $id == 1); 1594 1595 local $dbh->{AutoCommit} = 0; 1596 local $dbh->{RaiseError} = 1; 1597 1598 # Wrap all the SQL in a transaction 1599 eval { 1600 if ($inherit) { 1601 1602 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='t',permission_id=? ". 1603 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($parpermid, $id) ); 1604 $dbh->do("DELETE FROM permissions WHERE permission_id=?", undef, ($permid) ); 1605 1606 } else { 1607 1608 if ($wasinherited) { # munge new permission entry in if we're switching from inherited perms 1609 ##fixme: need to add semirecursive bit to properly munge inherited permission ID on subgroups and users 1610 # ... if'n'when we have groups with fully inherited permissions. 1611 # SQL is coo 1612 $dbh->do("INSERT INTO permissions ($permlist,".($type eq 'user' ? 'user' : 'group')."_id) ". 1613 "SELECT $permlist,? FROM permissions WHERE permission_id=?", undef, ($id,$permid) ); 1614 ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions ". 1615 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($id) ); 1616 $dbh->do("UPDATE ".($type eq 'user' ? 'users' : 'groups')." SET inherit_perm='f',permission_id=? ". 1617 "WHERE ".($type eq 'user' ? 'user' : 'group')."_id=?", undef, ($permid, $id) ); 1618 } 1619 1620 # and now set the permissions we were passed 1621 foreach (@permtypes) { 1622 if (defined ($newperms->{$_})) { 1623 $dbh->do("UPDATE permissions SET $_=? WHERE permission_id=?", undef, ($newperms->{$_},$permid) ); 1624 } 1625 } 1626 1627 } # (inherited->)? custom 1628 1629 if ($type eq 'user') { 1630 $resultmsg = "Updated permissions for user $name"; 1631 } else { 1632 $resultmsg = "Updated default permissions for group $name"; 1633 } 1634 _log($dbh, (group_id => ($type eq 'user' ? $parid : $id), entry => $resultmsg)); 1635 $dbh->commit; 1636 }; # end eval 1637 if ($@) { 1638 my $msg = $@; 1639 eval { $dbh->rollback; }; 1640 return ('FAIL',"Error changing permissions: $msg"); 1641 } 1642 1643 return ('OK',$resultmsg); 1644 } # end changePermissions() 1645 1646 1647 ## DNSDB::comparePermissions() 1648 # Compare two permission hashes 1649 # Returns '>', '<', '=', '!' 1650 sub comparePermissions { 1651 my $p1 = shift; 1652 my $p2 = shift; 1653 1654 my $retval = '='; # assume equality until proven otherwise 1655 1656 no warnings "uninitialized"; 1657 1658 foreach (@permtypes) { 1659 next if $p1->{$_} == $p2->{$_}; # equal is good 1660 if ($p1->{$_} && !$p2->{$_}) { 1661 if ($retval eq '<') { # if we've already found an unequal pair where 1662 $retval = '!'; # $p2 has more access, and we now find a pair 1663 last; # where $p1 has more access, the overall access 1664 } # is neither greater or lesser, it's unequal. 1665 $retval = '>'; 1666 } 1667 if (!$p1->{$_} && $p2->{$_}) { 1668 if ($retval eq '>') { # if we've already found an unequal pair where 1669 $retval = '!'; # $p1 has more access, and we now find a pair 1670 last; # where $p2 has more access, the overall access 1671 } # is neither greater or lesser, it's unequal. 1672 $retval = '<'; 1673 } 1674 } 1675 return $retval; 1676 } # end comparePermissions() 1677 1678 1679 ## DNSDB::changeGroup() 1680 # Change group ID of an entity 1681 # Takes a database handle, entity type, entity ID, and new group ID 1682 sub changeGroup { 1683 my $dbh = shift; 1684 my $type = shift; 1685 my $id = shift; 1686 my $newgrp = shift; 1687 1688 ##fixme: fail on not enough args 1689 #return ('FAIL', "Missing 1690 1691 return ('FAIL', "Can't change the group of a $type") 1692 unless grep /^$type$/, ('domain','revzone','user','group'); # could be extended for defrecs? 1693 1694 # Collect some names for logging and messages 1695 my $entname; 1696 if ($type eq 'domain') { 1697 $entname = domainName($dbh, $id); 1698 } elsif ($type eq 'revzone') { 1699 $entname = revName($dbh, $id); 1700 } elsif ($type eq 'user') { 1701 $entname = userFullName($dbh, $id, '%u'); 1702 } elsif ($type eq 'group') { 1703 $entname = groupName($dbh, $id); 1704 } 1705 1706 my ($oldgid) = $dbh->selectrow_array("SELECT group_id FROM $par_tbl{$type} WHERE $id_col{$type}=?", 1707 undef, ($id)); 1708 my $oldgname = groupName($dbh, $oldgid); 1709 my $newgname = groupName($dbh, $newgrp); 1710 1711 return ('FAIL', "Can't move things into a group that doesn't exist") if !$newgname; 1712 1713 return ('WARN', "Nothing to do, new group is the same as the old group") if $oldgid == $newgrp; 1714 1715 # Allow transactions, and raise an exception on errors so we can catch it later. 1716 # Use local to make sure these get "reset" properly on exiting this block 1717 local $dbh->{AutoCommit} = 0; 1718 local $dbh->{RaiseError} = 1; 1719 1720 eval { 1721 $dbh->do("UPDATE $par_tbl{$type} SET group_id=? WHERE $id_col{$type}=?", undef, ($newgrp, $id)); 1722 # Log the change in both the old and new groups 1723 _log($dbh, (group_id => $oldgid, entry => "Moved $type $entname from $oldgname to $newgname")); 1724 _log($dbh, (group_id => $newgrp, entry => "Moved $type $entname from $oldgname to $newgname")); 1725 $dbh->commit; 1726 }; 1727 if ($@) { 1728 my $msg = $@; 1729 eval { $dbh->rollback; }; 1730 if ($config{log_failures}) { 1731 _log($dbh, (group_id => $oldgid, entry => "Error moving $type $entname to $newgname: $msg")); 1732 $dbh->commit; # since we enabled transactions earlier 1733 } 1734 return ('FAIL',"Error moving $type $entname to $newgname: $msg"); 1735 } 1736 1737 return ('OK',"Moved $type $entname from $oldgname to $newgname"); 1738 } # end changeGroup() 1739 1740 1741 ## 1742 ## Processing subs 1743 ## 1744 1745 ## DNSDB::addDomain() 1746 # Add a domain 1747 # Takes a database handle, domain name, numeric group, boolean(ish) state (active/inactive), 1748 # and user info hash (for logging). 1749 # Returns a status code and message 1750 sub addDomain { 1751 $errstr = ''; 1752 my $dbh = shift; 1753 return ('FAIL',"Need database handle") if !$dbh; 1754 my $domain = shift; 1755 return ('FAIL',"Domain must not be blank") if !$domain; 1756 my $group = shift; 1757 return ('FAIL',"Need group") if !defined($group); 1758 my $state = shift; 1759 return ('FAIL',"Need domain status") if !defined($state); 1760 1761 $state = 1 if $state =~ /^active$/; 1762 $state = 1 if $state =~ /^on$/; 1763 $state = 0 if $state =~ /^inactive$/; 1764 $state = 0 if $state =~ /^off$/; 1765 1766 return ('FAIL',"Invalid domain status") if $state !~ /^\d+$/; 1767 1768 return ('FAIL', "Invalid characters in domain") if $domain !~ /^[a-zA-Z0-9_.-]+$/; 1769 1770 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)"); 1771 my $dom_id; 1772 1773 # quick check to start to see if we've already got one 1774 $sth->execute($domain); 1775 ($dom_id) = $sth->fetchrow_array; 1776 1777 return ('FAIL', "Domain already exists") if $dom_id; 1778 1779 # Allow transactions, and raise an exception on errors so we can catch it later. 1780 # Use local to make sure these get "reset" properly on exiting this block 1781 local $dbh->{AutoCommit} = 0; 1782 local $dbh->{RaiseError} = 1; 1783 1784 # Wrap all the SQL in a transaction 1785 eval { 1786 # insert the domain... 1787 $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($domain, $group, $state)); 1788 1789 # get the ID... 1790 ($dom_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)", 1791 undef, ($domain)); 1792 1793 _log($dbh, (domain_id => $dom_id, group_id => $group, 1794 entry => "Added ".($state ? 'active' : 'inactive')." domain $domain")); 1795 1796 # ... and now we construct the standard records from the default set. NB: group should be variable. 1797 my $sth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?"); 1798 my $sth_in = $dbh->prepare("INSERT INTO records (domain_id,host,type,val,distance,weight,port,ttl)". 1799 " VALUES ($dom_id,?,?,?,?,?,?,?)"); 1800 $sth->execute($group); 1801 while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $sth->fetchrow_array()) { 1802 $host =~ s/DOMAIN/$domain/g; 1803 $val =~ s/DOMAIN/$domain/g; 1804 $sth_in->execute($host,$type,$val,$dist,$weight,$port,$ttl); 1805 if ($typemap{$type} eq 'SOA') { 1806 my @tmp1 = split /:/, $host; 1807 my @tmp2 = split /:/, $val; 1808 _log($dbh, (domain_id => $dom_id, group_id => $group, 1809 entry => "[new $domain] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 1810 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); 1811 } else { 1812 my $logentry = "[new $domain] Added record '$host $typemap{$type}"; 1813 $logentry .= " [distance $dist]" if $typemap{$type} eq 'MX'; 1814 $logentry .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$type} eq 'SRV'; 1815 _log($dbh, (domain_id => $dom_id, group_id => $group, 1816 entry => $logentry." $val', TTL $ttl")); 1817 } 1818 } 1819 1820 # once we get here, we should have suceeded. 1821 $dbh->commit; 1822 }; # end eval 1823 1824 if ($@) { 1825 my $msg = $@; 1826 eval { $dbh->rollback; }; 1827 _log($dbh, (group_id => $group, entry => "Failed adding domain $domain ($msg)")) 1828 if $config{log_failures}; 1829 $dbh->commit; # since we enabled transactions earlier 1830 return ('FAIL',$msg); 1831 } else { 1832 return ('OK',$dom_id); 1833 } 1834 } # end addDomain 1835 1836 1837 ## DNSDB::delZone() 1838 # Delete a forward or reverse zone. 1839 # Takes a database handle, zone ID, and forward/reverse flag. 1840 # for now, just delete the records, then the domain. 1841 # later we may want to archive it in some way instead (status code 2, for example?) 1842 sub delZone { 1843 my $dbh = shift; 1844 my $zoneid = shift; 1845 my $revrec = shift; 1846 1847 # Allow transactions, and raise an exception on errors so we can catch it later. 1848 # Use local to make sure these get "reset" properly on exiting this block 1849 local $dbh->{AutoCommit} = 0; 1850 local $dbh->{RaiseError} = 1; 1851 1852 my $msg = ''; 1853 my $failmsg = ''; 1854 my $zone = ($revrec eq 'n' ? domainName($dbh, $zoneid) : revName($dbh, $zoneid)); 1855 1856 return ('FAIL', ($revrec eq 'n' ? 'Domain' : 'Reverse zone')." ID $zoneid doesn't exist") if !$zone; 1857 1858 # Set this up here since we may use if if $config{log_failures} is enabled 1859 my %loghash; 1860 $loghash{domain_id} = $zoneid if $revrec eq 'n'; 1861 $loghash{rdns_id} = $zoneid if $revrec eq 'y'; 1862 $loghash{group_id} = parentID($dbh, 1863 (id => $zoneid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ); 1864 1865 # Wrap all the SQL in a transaction 1866 eval { 1867 # Disentangle custom record types before removing the 1868 # ones that are only in the zone to be deleted 1869 if ($revrec eq 'n') { 1870 my $sth = $dbh->prepare("UPDATE records SET type=?,domain_id=0 WHERE domain_id=? AND type=?"); 1871 $failmsg = "Failure converting multizone types to single-zone"; 1872 $sth->execute($reverse_typemap{PTR}, $zoneid, 65280); 1873 $sth->execute($reverse_typemap{PTR}, $zoneid, 65281); 1874 $sth->execute(65282, $zoneid, 65283); 1875 $sth->execute(65282, $zoneid, 65284); 1876 $failmsg = "Failure removing domain records"; 1877 $dbh->do("DELETE FROM records WHERE domain_id=?", undef, ($zoneid)); 1878 $failmsg = "Failure removing domain"; 1879 $dbh->do("DELETE FROM domains WHERE domain_id=?", undef, ($zoneid)); 1880 } else { 1881 my $sth = $dbh->prepare("UPDATE records SET type=?,rdns_id=0 WHERE rdns_id=? AND type=?"); 1882 $failmsg = "Failure converting multizone types to single-zone"; 1883 $sth->execute($reverse_typemap{A}, $zoneid, 65280); 1884 $sth->execute($reverse_typemap{AAAA}, $zoneid, 65281); 1885 # We don't have an "A template" or "AAAA template" type, although it might be useful for symmetry. 1886 # $sth->execute(65286?, $zoneid, 65283); 1887 # $sth->execute(65286?, $zoneid, 65284); 1888 $failmsg = "Failure removing reverse records"; 1889 $dbh->do("DELETE FROM records WHERE rdns_id=?", undef, ($zoneid)); 1890 $failmsg = "Failure removing reverse zone"; 1891 $dbh->do("DELETE FROM revzones WHERE rdns_id=?", undef, ($zoneid)); 1892 } 1893 1894 $msg = "Deleted ".($revrec eq 'n' ? 'domain' : 'reverse zone')." $zone"; 1895 $loghash{entry} = $msg; 1896 _log($dbh, %loghash); 1897 1898 # once we get here, we should have suceeded. 1899 $dbh->commit; 1900 }; # end eval 1901 1902 if ($@) { 1903 $msg = $@; 1904 eval { $dbh->rollback; }; 1905 $loghash{entry} = "Error deleting $zone: $msg ($failmsg)"; 1906 if ($config{log_failures}) { 1907 _log($dbh, %loghash); 1908 $dbh->commit; # since we enabled transactions earlier 1909 } 1910 return ('FAIL', $loghash{entry}); 1911 } else { 1912 return ('OK', $msg); 1913 } 1914 1915 } # end delZone() 1916 1917 1918 ## DNSDB::domainName() 1919 # Return the domain name based on a domain ID 1920 # Takes a database handle and the domain ID 1921 # Returns the domain name or undef on failure 1922 sub domainName { 1923 $errstr = ''; 1924 my $dbh = shift; 1925 my $domid = shift; 1926 my ($domname) = $dbh->selectrow_array("SELECT domain FROM domains WHERE domain_id=?", undef, ($domid) ); 1927 $errstr = $DBI::errstr if !$domname; 1928 return $domname if $domname; 1929 } # end domainName() 1930 1931 1932 ## DNSDB::revName() 1933 # Return the reverse zone name based on an rDNS ID 1934 # Takes a database handle and the rDNS ID 1935 # Returns the reverse zone name or undef on failure 1936 sub revName { 1937 $errstr = ''; 1938 my $dbh = shift; 1939 my $revid = shift; 1940 my ($revname) = $dbh->selectrow_array("SELECT revnet FROM revzones WHERE rdns_id=?", undef, ($revid) ); 1941 $errstr = $DBI::errstr if !$revname; 1942 return $revname if $revname; 1943 } # end revName() 1944 1945 1946 ## DNSDB::domainID() 1947 # Takes a database handle and domain name 1948 # Returns the domain ID number 1949 sub domainID { 1950 $errstr = ''; 1951 my $dbh = shift; 1952 my $domain = shift; 1953 my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)", 1954 undef, ($domain) ); 1955 $errstr = $DBI::errstr if !$domid; 1956 return $domid if $domid; 1957 } # end domainID() 1958 1959 1960 ## DNSDB::revID() 1961 # Takes a database handle and reverse zone name 1962 # Returns the rDNS ID number 1963 sub revID { 1964 $errstr = ''; 1965 my $dbh = shift; 1966 my $revzone = shift; 1967 my ($revid) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ($revzone) ); 1968 $errstr = $DBI::errstr if !$revid; 1969 return $revid if $revid; 1970 } # end revID() 1971 1972 1973 ## DNSDB::addRDNS 1974 # Adds a reverse DNS zone 1975 # Takes a database handle, CIDR block, reverse DNS pattern, numeric group, 1976 # and boolean(ish) state (active/inactive) 1977 # Returns a status code and message 1978 sub addRDNS { 1979 my $dbh = shift; 1980 my $zone = NetAddr::IP->new(shift); 1981 return ('FAIL',"Zone name must be a valid CIDR netblock") unless ($zone && $zone->addr !~ /^0/); 1982 my $revpatt = shift; # construct a custom (A/AAAA+)? PTR template record 1983 my $group = shift; 1984 my $state = shift; 1985 1986 $state = 1 if $state =~ /^active$/; 1987 $state = 1 if $state =~ /^on$/; 1988 $state = 0 if $state =~ /^inactive$/; 1989 $state = 0 if $state =~ /^off$/; 1990 1991 return ('FAIL',"Invalid zone status") if $state !~ /^\d+$/; 1992 1993 # quick check to start to see if we've already got one 1994 my ($rdns_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", undef, ("$zone")); 1995 1996 return ('FAIL', "Zone already exists") if $rdns_id; 1997 1998 # Allow transactions, and raise an exception on errors so we can catch it later. 1999 # Use local to make sure these get "reset" properly on exiting this block 2000 local $dbh->{AutoCommit} = 0; 2001 local $dbh->{RaiseError} = 1; 2002 2003 my $warnstr = ''; 2004 my $defttl = 3600; # 1 hour should be reasonable. And unless things have gone horribly 2005 # wrong, we should have a value to override this anyway. 2006 2007 # Wrap all the SQL in a transaction 2008 eval { 2009 # insert the domain... 2010 $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($zone, $group, $state)); 2011 2012 # get the ID... 2013 ($rdns_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); 2014 2015 _log($dbh, (rdns_id => $rdns_id, group_id => $group, 2016 entry => "Added ".($state ? 'active' : 'inactive')." reverse zone $zone")); 2017 2018 # ... and now we construct the standard records from the default set. NB: group should be variable. 2019 my $sth = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?"); 2020 my $sth_in = $dbh->prepare("INSERT INTO records (rdns_id,domain_id,host,type,val,ttl)". 2021 " VALUES ($rdns_id,?,?,?,?,?)"); 2022 $sth->execute($group); 2023 while (my ($host,$type,$val,$ttl) = $sth->fetchrow_array()) { 2024 # Silently skip v4/v6 mismatches. This is not an error, this is expected. 2025 if ($zone->{isv6}) { 2026 next if ($type == 65280 || $type == 65283); 2027 } else { 2028 next if ($type == 65281 || $type == 65284); 2029 } 2030 2031 $host =~ s/ADMINDOMAIN/$config{domain}/g; 2032 2033 # Check to make sure the IP stubs will fit in the zone. Under most usage failures here should be rare. 2034 # On failure, tack a note on to a warning string and continue without adding this record. 2035 # While we're at it, we substitute $zone for ZONE in the value. 2036 if ($val eq 'ZONE') { 2037 next if $revpatt; # If we've got a pattern, we skip the default record version. 2038 ##fixme? do we care if we have multiple whole-zone templates? 2039 $val = $zone->network; 2040 } elsif ($val =~ /ZONE/) { 2041 my $tmpval = $val; 2042 $tmpval =~ s/ZONE//; 2043 # Bend the rules and allow single-trailing-number PTR or PTR template records to be inserted 2044 # as either v4 or v6. May make this an off-by-default config flag 2045 # Note that the origin records that may trigger this **SHOULD** already have ZONE,\d 2046 if ($type == 12 || $type == 65282) { 2047 $tmpval =~ s/[,.]/::/ if ($tmpval =~ /^[,.]\d+$/ && $zone->{isv6}); 2048 $tmpval =~ s/[,:]+/./ if ($tmpval =~ /^(?:,|::)\d+$/ && !$zone->{isv6}); 2049 } 2050 my $addr; 2051 if (_ipparent($dbh, 'n', 'y', \$tmpval, $rdns_id, \$addr)) { 2052 $val = $addr->addr; 2053 } else { 2054 $warnstr .= "\nDefault record '$val $typemap{$type} $host' doesn't fit in $zone, skipping"; 2055 next; 2056 } 2057 } 2058 2059 # Substitute $zone for ZONE in the hostname, but only for non-NS records. 2060 # NS records get this substitution on the value instead. 2061 $host = _ZONE($zone, $host) if $type != 2; 2062 2063 # Fill in the forward domain ID if we can find it, otherwise: 2064 # Coerce type down to PTR or PTR template if we can't 2065 my $domid = 0; 2066 if ($type >= 65280) { 2067 if (!($domid = _hostparent($dbh, $host))) { 2068 $warnstr .= "\nRecord added as PTR instead of $typemap{$type}; domain not found for $host"; 2069 $type = $reverse_typemap{PTR}; 2070 $domid = 0; # just to be explicit. 2071 } 2072 } 2073 2074 $sth_in->execute($domid,$host,$type,$val,$ttl); 2075 2076 if ($typemap{$type} eq 'SOA') { 2077 my @tmp1 = split /:/, $host; 2078 my @tmp2 = split /:/, $val; 2079 _log($dbh, (rdns_id => $rdns_id, group_id => $group, 2080 entry => "[new $zone] Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 2081 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl")); 2082 $defttl = $tmp2[3]; 2083 } else { 2084 my $logentry = "[new $zone] Added record '$host $typemap{$type}"; 2085 _log($dbh, (rdns_id => $rdns_id, domain_id => $domid, group_id => $group, 2086 entry => $logentry." $val', TTL $ttl")); 2087 } 2088 } 2089 2090 # Generate record based on provided pattern. 2091 if ($revpatt) { 2092 my $host; 2093 my $type = ($zone->{isv6} ? 65284 : 65283); 2094 my $val = $zone->network; 2095 2096 # Substitute $zone for ZONE in the hostname. 2097 $host = _ZONE($zone, $revpatt); 2098 2099 my $domid = 0; 2100 if (!($domid = _hostparent($dbh, $host))) { 2101 $warnstr .= "\nDefault pattern added as PTR template instead of $typemap{$type}; domain not found for $host"; 2102 $type = 65282; 2103 $domid = 0; # just to be explicit. 2104 } 2105 2106 $sth_in->execute($domid,$host,$type,$val,$defttl); 2107 my $logentry = "[new $zone] Added record '$host $typemap{$type}"; 2108 _log($dbh, (rdns_id => $rdns_id, domain_id => $domid, group_id => $group, 2109 entry => $logentry." $val', TTL $defttl from pattern")); 2110 } 2111 2112 # If there are warnings (presumably about default records skipped for cause) log them 2113 _log($dbh, (rdns_id => $rdns_id, group_id => $group, entry => "Warning(s) adding $zone:$warnstr")) 2114 if $warnstr; 2115 2116 # once we get here, we should have suceeded. 2117 $dbh->commit; 2118 }; # end eval 2119 2120 if ($@) { 2121 my $msg = $@; 2122 eval { $dbh->rollback; }; 2123 _log($dbh, (group_id => $group, entry => "Failed adding reverse zone $zone ($msg)")) 2124 if $config{log_failures}; 2125 $dbh->commit; # since we enabled transactions earlier 2126 return ('FAIL',$msg); 2127 } else { 2128 my $retcode = 'OK'; 2129 if ($warnstr) { 2130 $resultstr = $warnstr; 2131 $retcode = 'WARN'; 2132 } 2133 return ($retcode, $rdns_id); 2134 } 2135 2136 } # end addRDNS() 2137 2138 2139 ## DNSDB::getZoneCount 2140 # Get count of zones in group or groups 2141 # Takes a database handle and hash containing: 2142 # - the "current" group 2143 # - an array of "acceptable" groups 2144 # - a flag for forward/reverse zones 2145 # - Optionally accept a "starts with" and/or "contains" filter argument 2146 # Returns an integer count of the resulting zone list. 2147 sub getZoneCount { 2148 my $dbh = shift; 2149 2150 my %args = @_; 2151 2152 my @filterargs; 2153 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 2154 push @filterargs, "^$args{startwith}" if $args{startwith}; 2155 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones 2156 push @filterargs, $args{filter} if $args{filter}; 2157 2158 my $sql; 2159 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read 2160 if ($args{revrec} eq 'n') { 2161 $sql = "SELECT count(*) FROM domains". 2162 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2163 ($args{startwith} ? " AND domain ~* ?" : ''). 2164 ($args{filter} ? " AND domain ~* ?" : ''); 2165 } else { 2166 $sql = "SELECT count(*) FROM revzones". 2167 " WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2168 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). 2169 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); 2170 } 2171 my ($count) = $dbh->selectrow_array($sql, undef, @filterargs); 2172 return $count; 2173 } # end getZoneCount() 2174 2175 2176 ## DNSDB::getZoneList() 2177 # Get a list of zones in the specified group(s) 2178 # Takes the same arguments as getZoneCount() above 2179 # Returns a reference to an array of hashrefs suitable for feeding to HTML::Template 2180 sub getZoneList { 2181 my $dbh = shift; 2182 2183 my %args = @_; 2184 2185 my @zonelist; 2186 2187 $args{sortorder} = 'ASC' if !grep /^$args{sortorder}$/, ('ASC','DESC'); 2188 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 2189 2190 my @filterargs; 2191 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 2192 push @filterargs, "^$args{startwith}" if $args{startwith}; 2193 $args{filter} =~ s/\./\[\.\]/g if $args{filter}; # only match literal dots, usually in reverse zones 2194 push @filterargs, $args{filter} if $args{filter}; 2195 2196 my $sql; 2197 # Not as compact, and fix-me-twice if the common bits get wrong, but much easier to read 2198 if ($args{revrec} eq 'n') { 2199 $args{sortby} = 'domain' if !grep /^$args{sortby}$/, ('domain','group','status'); 2200 $sql = "SELECT domain_id,domain,status,groups.group_name AS group FROM domains". 2201 " INNER JOIN groups ON domains.group_id=groups.group_id". 2202 " WHERE domains.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2203 ($args{startwith} ? " AND domain ~* ?" : ''). 2204 ($args{filter} ? " AND domain ~* ?" : ''); 2205 } else { 2206 ##fixme: arguably startwith here is irrelevant. depends on the UI though. 2207 $args{sortby} = 'revnet' if !grep /^$args{sortby}$/, ('revnet','group','status'); 2208 $sql = "SELECT rdns_id,revnet,status,groups.group_name AS group FROM revzones". 2209 " INNER JOIN groups ON revzones.group_id=groups.group_id". 2210 " WHERE revzones.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2211 ($args{startwith} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''). 2212 ($args{filter} ? " AND CAST(revnet AS VARCHAR) ~* ?" : ''); 2213 } 2214 # A common tail. 2215 $sql .= " ORDER BY ".($args{sortby} eq 'group' ? 'groups.group_name' : $args{sortby})." $args{sortorder} ". 2216 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage}". 2217 " OFFSET ".$args{offset}*$config{perpage}); 2218 my $sth = $dbh->prepare($sql); 2219 $sth->execute(@filterargs); 2220 my $rownum = 0; 2221 2222 while (my @data = $sth->fetchrow_array) { 2223 my %row; 2224 $row{domain_id} = $data[0]; 2225 $row{domain} = $data[1]; 2226 $row{status} = $data[2]; 2227 $row{group} = $data[3]; 2228 push @zonelist, \%row; 2229 } 2230 2231 return \@zonelist; 2232 } # end getZoneList() 2233 2234 2235 ## DNSDB::getZoneLocation() 2236 # Retrieve the default location for a zone. 2237 # Takes a database handle, forward/reverse flag, and zone ID 2238 sub getZoneLocation { 2239 my $dbh = shift; 2240 my $revrec = shift; 2241 my $zoneid = shift; 2242 2243 my ($loc) = $dbh->selectrow_array("SELECT default_location FROM ". 2244 ($revrec eq 'n' ? 'domains WHERE domain_id = ?' : 'revzones WHERE rdns_id = ?'), 2245 undef, ($zoneid)); 2246 return $loc; 2247 } # end getZoneLocation() 2248 2249 2250 ## DNSDB::addGroup() 2251 # Add a group 2252 # Takes a database handle, group name, parent group, hashref for permissions, 2253 # and optional template-vs-cloneme flag for the default records 2254 # Returns a status code and message 2255 sub addGroup { 2256 $errstr = ''; 2257 my $dbh = shift; 2258 my $groupname = shift; 2259 my $pargroup = shift; 2260 my $permissions = shift; 2261 2262 # 0 indicates "custom", hardcoded. 2263 # Any other value clones that group's default records, if it exists. 2264 my $inherit = shift || 0; 2265 ##fixme: need a flag to indicate clone records or <?> ? 2266 2267 # Allow transactions, and raise an exception on errors so we can catch it later. 2268 # Use local to make sure these get "reset" properly on exiting this block 2269 local $dbh->{AutoCommit} = 0; 2270 local $dbh->{RaiseError} = 1; 2271 2272 my ($group_id) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($groupname)); 2273 2274 return ('FAIL', "Group already exists") if $group_id; 2275 2276 # Wrap all the SQL in a transaction 2277 eval { 2278 $dbh->do("INSERT INTO groups (parent_group_id,group_name) VALUES (?,?)", undef, ($pargroup, $groupname) ); 2279 2280 my ($groupid) = $dbh->selectrow_array("SELECT currval('groups_group_id_seq')"); 2281 2282 # We work through the whole set of permissions instead of specifying them so 2283 # that when we add a new permission, we don't have to change the code anywhere 2284 # that doesn't explicitly deal with that specific permission. 2285 my @permvals; 2286 foreach (@permtypes) { 2287 if (!defined ($permissions->{$_})) { 2288 push @permvals, 0; 2289 } else { 2290 push @permvals, $permissions->{$_}; 2291 } 2292 } 2293 $dbh->do("INSERT INTO permissions (group_id,$permlist) values (?".',?'x($#permtypes+1).")", 2294 undef, ($groupid, @permvals) ); 2295 my ($permid) = $dbh->selectrow_array("SELECT currval('permissions_permission_id_seq')"); 2296 $dbh->do("UPDATE groups SET permission_id=$permid WHERE group_id=$groupid"); 2297 2298 # Default records 2299 my $sthf = $dbh->prepare("INSERT INTO default_records (group_id,host,type,val,distance,weight,port,ttl) ". 2300 "VALUES ($groupid,?,?,?,?,?,?,?)"); 2301 my $sthr = $dbh->prepare("INSERT INTO default_rev_records (group_id,host,type,val,ttl) ". 2302 "VALUES ($groupid,?,?,?,?)"); 2303 if ($inherit) { 2304 # Duplicate records from parent. Actually relying on inherited records feels 2305 # very fragile, and it would be problematic to roll over at a later time. 2306 my $sth2 = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl FROM default_records WHERE group_id=?"); 2307 $sth2->execute($pargroup); 2308 while (my @clonedata = $sth2->fetchrow_array) { 2309 $sthf->execute(@clonedata); 2310 } 2311 # And now the reverse records 2312 $sth2 = $dbh->prepare("SELECT host,type,val,ttl FROM default_rev_records WHERE group_id=?"); 2313 $sth2->execute($pargroup); 2314 while (my @clonedata = $sth2->fetchrow_array) { 2315 $sthr->execute(@clonedata); 2316 } 2317 } else { 2318 ##fixme: Hardcoding is Bad, mmmmkaaaay? 2319 # reasonable basic defaults for SOA, MX, NS, and minimal hosting 2320 # could load from a config file, but somewhere along the line we need hardcoded bits. 2321 $sthf->execute('ns1.example.com:hostmaster.example.com', 6, '10800:3600:604800:10800', 0, 0, 0, 86400); 2322 $sthf->execute('DOMAIN', 1, '192.168.4.2', 0, 0, 0, 7200); 2323 $sthf->execute('DOMAIN', 15, 'mx.example.com', 10, 0, 0, 7200); 2324 $sthf->execute('DOMAIN', 2, 'ns1.example.com', 0, 0, 0, 7200); 2325 $sthf->execute('DOMAIN', 2, 'ns2.example.com', 0, 0, 0, 7200); 2326 $sthf->execute('www.DOMAIN', 5, 'DOMAIN', 0, 0, 0, 7200); 2327 # reasonable basic defaults for generic reverse zone. Same as initial SQL tabledef. 2328 $sthr->execute('hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN', 6, '10800:3600:604800:10800', 86400); 2329 $sthr->execute('unused-%r.ADMINDOMAIN', 65283, 'ZONE', 3600); 2330 } 2331 2332 _log($dbh, (group_id => $pargroup, entry => "Added group $groupname") ); 2333 2334 # once we get here, we should have suceeded. 2335 $dbh->commit; 2336 }; # end eval 2337 2338 if ($@) { 2339 my $msg = $@; 2340 eval { $dbh->rollback; }; 2341 if ($config{log_failures}) { 2342 _log($dbh, (group_id => $pargroup, entry => "Failed to add group $groupname: $msg") ); 2343 $dbh->commit; 2344 } 2345 return ('FAIL',$msg); 2346 } 2347 2348 return ('OK','OK'); 2349 } # end addGroup() 2350 2351 2352 ## DNSDB::delGroup() 2353 # Delete a group. 2354 # Takes a group ID 2355 # Returns a status code and message 2356 sub delGroup { 2357 my $dbh = shift; 2358 my $groupid = shift; 2359 2360 # Allow transactions, and raise an exception on errors so we can catch it later. 2361 # Use local to make sure these get "reset" properly on exiting this block 2362 local $dbh->{AutoCommit} = 0; 2363 local $dbh->{RaiseError} = 1; 2364 2365 ##fixme: locate "knowable" error conditions and deal with them before the eval 2366 # ... or inside, whatever. 2367 # -> domains still exist in group 2368 # -> ... 2369 my $failmsg = ''; 2370 my $resultmsg = ''; 2371 2372 # collect some pieces for logging and error messages 2373 my $groupname = groupName($dbh,$groupid); 2374 my $parid = parentID($dbh, (id => $groupid, type => 'group')); 2375 2376 # Wrap all the SQL in a transaction 2377 eval { 2378 # Check for Things in the group 2379 $failmsg = "Can't remove group $groupname"; 2380 my ($grpcnt) = $dbh->selectrow_array("SELECT count(*) FROM groups WHERE parent_group_id=?", undef, ($groupid)); 2381 die "$grpcnt groups still in group\n" if $grpcnt; 2382 my ($domcnt) = $dbh->selectrow_array("SELECT count(*) FROM domains WHERE group_id=?", undef, ($groupid)); 2383 die "$domcnt domains still in group\n" if $domcnt; 2384 my ($usercnt) = $dbh->selectrow_array("SELECT count(*) FROM users WHERE group_id=?", undef, ($groupid)); 2385 die "$usercnt users still in group\n" if $usercnt; 2386 2387 $failmsg = "Failed to delete default records for $groupname"; 2388 $dbh->do("DELETE from default_records WHERE group_id=?", undef, ($groupid)); 2389 $failmsg = "Failed to delete default reverse records for $groupname"; 2390 $dbh->do("DELETE from default_rev_records WHERE group_id=?", undef, ($groupid)); 2391 $failmsg = "Failed to remove group $groupname"; 2392 $dbh->do("DELETE from groups WHERE group_id=?", undef, ($groupid)); 2393 2394 _log($dbh, (group_id => $parid, entry => "Deleted group $groupname")); 2395 $resultmsg = "Deleted group $groupname"; 2396 2397 # once we get here, we should have suceeded. 2398 $dbh->commit; 2399 }; # end eval 2400 2401 if ($@) { 2402 my $msg = $@; 2403 eval { $dbh->rollback; }; 2404 if ($config{log_failures}) { 2405 _log($dbh, (group_id => $parid, entry => "$failmsg: $msg")); 2406 $dbh->commit; # since we enabled transactions earlier 2407 } 2408 return ('FAIL',"$failmsg: $msg"); 2409 } 2410 2411 return ('OK',$resultmsg); 2412 } # end delGroup() 2413 2414 2415 ## DNSDB::getChildren() 2416 # Get a list of all groups whose parent^n is group <n> 2417 # Takes a database handle, group ID, reference to an array to put the group IDs in, 2418 # and an optional flag to return only immediate children or all children-of-children 2419 # default to returning all children 2420 # Calls itself 2421 sub getChildren { 2422 $errstr = ''; 2423 my $dbh = shift; 2424 my $rootgroup = shift; 2425 my $groupdest = shift; 2426 my $immed = shift || 'all'; 2427 2428 # special break for default group; otherwise we get stuck. 2429 if ($rootgroup == 1) { 2430 # by definition, group 1 is the Root Of All Groups 2431 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE NOT (group_id=1)". 2432 ($immed ne 'all' ? " AND parent_group_id=1" : '')." ORDER BY group_name"); 2433 $sth->execute; 2434 while (my @this = $sth->fetchrow_array) { 2435 push @$groupdest, @this; 2436 } 2437 } else { 2438 my $sth = $dbh->prepare("SELECT group_id FROM groups WHERE parent_group_id=? ORDER BY group_name"); 2439 $sth->execute($rootgroup); 2440 return if $sth->rows == 0; 2441 my @grouplist; 2442 while (my ($group) = $sth->fetchrow_array) { 2443 push @$groupdest, $group; 2444 getChildren($dbh,$group,$groupdest) if $immed eq 'all'; 2445 } 2446 } 2447 } # end getChildren() 2448 2449 2450 ## DNSDB::groupName() 2451 # Return the group name based on a group ID 2452 # Takes a database handle and the group ID 2453 # Returns the group name or undef on failure 2454 sub groupName { 2455 $errstr = ''; 2456 my $dbh = shift; 2457 my $groupid = shift; 2458 my $sth = $dbh->prepare("SELECT group_name FROM groups WHERE group_id=?"); 2459 $sth->execute($groupid); 2460 my ($groupname) = $sth->fetchrow_array(); 2461 $errstr = $DBI::errstr if !$groupname; 2462 return $groupname if $groupname; 2463 } # end groupName 2464 2465 2466 ## DNSDB::getGroupCount() 2467 # Get count of subgroups in group or groups 2468 # Takes a database handle and hash containing: 2469 # - the "current" group 2470 # - an array of "acceptable" groups 2471 # - Optionally accept a "starts with" and/or "contains" filter argument 2472 # Returns an integer count of the resulting group list. 2473 sub getGroupCount { 2474 my $dbh = shift; 2475 2476 my %args = @_; 2477 2478 my @filterargs; 2479 2480 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 2481 push @filterargs, "^$args{startwith}" if $args{startwith}; 2482 push @filterargs, $args{filter} if $args{filter}; 2483 2484 my $sql = "SELECT count(*) FROM groups ". 2485 "WHERE parent_group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2486 ($args{startwith} ? " AND group_name ~* ?" : ''). 2487 ($args{filter} ? " AND group_name ~* ?" : ''); 2488 my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) ); 2489 $errstr = $dbh->errstr if !$count; 2490 return $count; 2491 } # end getGroupCount 2492 2493 2494 ## DNSDB::getGroupList() 2495 # Get a list of sub^n-groups in the specified group(s) 2496 # Takes the same arguments as getGroupCount() above 2497 # Returns an arrayref containing hashrefs suitable for feeding straight to HTML::Template 2498 sub getGroupList { 2499 my $dbh = shift; 2500 2501 my %args = @_; 2502 2503 my @filterargs; 2504 2505 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 2506 push @filterargs, "^$args{startwith}" if $args{startwith}; 2507 push @filterargs, $args{filter} if $args{filter}; 2508 2509 # protection against bad or missing arguments 2510 $args{sortorder} = 'ASC' if !$args{sortorder}; 2511 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 2512 2513 # munge sortby for columns in database 2514 $args{sortby} = 'g.group_name' if $args{sortby} eq 'group'; 2515 $args{sortby} = 'g2.group_name' if $args{sortby} eq 'parent'; 2516 2517 my $sql = q(SELECT g.group_id AS groupid, g.group_name AS groupname, g2.group_name AS pgroup 2518 FROM groups g 2519 INNER JOIN groups g2 ON g2.group_id=g.parent_group_id 2520 ). 2521 " WHERE g.parent_group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2522 ($args{startwith} ? " AND g.group_name ~* ?" : ''). 2523 ($args{filter} ? " AND g.group_name ~* ?" : ''). 2524 " GROUP BY g.group_id, g.group_name, g2.group_name ". 2525 " ORDER BY $args{sortby} $args{sortorder} ". 2526 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); 2527 my $glist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) ); 2528 $errstr = $dbh->errstr if !$glist; 2529 2530 # LEFT JOINs make the result set balloon beyond sanity just to include counts; 2531 # this means there's lots of crunching needed to trim the result set back down. 2532 # So instead we track the order of the groups, and push the counts into the 2533 # arrayref result separately. 2534 ##fixme: put this whole sub in a transaction? might be 2535 # needed for accurate results on very busy systems. 2536 ##fixme: large group lists need prepared statements? 2537 #my $ucsth = $dbh->prepare("SELECT count(*) FROM users WHERE group_id=?"); 2538 #my $dcsth = $dbh->prepare("SELECT count(*) FROM domains WHERE group_id=?"); 2539 #my $rcsth = $dbh->prepare("SELECT count(*) FROM revzones WHERE group_id=?"); 2540 foreach (@{$glist}) { 2541 my ($ucnt) = $dbh->selectrow_array("SELECT count(*) FROM users WHERE group_id=?", undef, ($$_{groupid})); 2542 $$_{nusers} = $ucnt; 2543 my ($dcnt) = $dbh->selectrow_array("SELECT count(*) FROM domains WHERE group_id=?", undef, ($$_{groupid})); 2544 $$_{ndomains} = $dcnt; 2545 my ($rcnt) = $dbh->selectrow_array("SELECT count(*) FROM revzones WHERE group_id=?", undef, ($$_{groupid})); 2546 $$_{nrevzones} = $rcnt; 2547 } 2548 2549 return $glist; 2550 } # end getGroupList 2551 2552 2553 ## DNSDB::groupID() 2554 # Return the group ID based on the group name 2555 # Takes a database handle and the group name 2556 # Returns the group ID or undef on failure 2557 sub groupID { 2558 $errstr = ''; 2559 my $dbh = shift; 2560 my $group = shift; 2561 my ($grpid) = $dbh->selectrow_array("SELECT group_id FROM groups WHERE group_name=?", undef, ($group) ); 2562 $errstr = $DBI::errstr if !$grpid; 2563 return $grpid if $grpid; 2564 } # end groupID() 2565 2566 2567 ## DNSDB::addUser() 2568 # Add a user. 2569 # Takes a DB handle, username, group ID, password, state (active/inactive). 2570 # Optionally accepts: 2571 # user type (user/admin) - defaults to user 2572 # permissions string - defaults to inherit from group 2573 # three valid forms: 2574 # i - Inherit permissions 2575 # c:<user_id> - Clone permissions from <user_id> 2576 # C:<permission list> - Set these specific permissions 2577 # first name - defaults to username 2578 # last name - defaults to blank 2579 # phone - defaults to blank (could put other data within column def) 2580 # Returns (OK,<uid>) on success, (FAIL,<message>) on failure 2581 sub addUser { 2582 $errstr = ''; 2583 my $dbh = shift; 2584 my $username = shift; 2585 my $group = shift; 2586 my $pass = shift; 2587 my $state = shift; 2588 2589 return ('FAIL', "Missing one or more required entries") if !defined($state); 2590 return ('FAIL', "Username must not be blank") if !$username; 2591 2592 # Munge in some alternate state values 2593 $state = 1 if $state =~ /^active$/; 2594 $state = 1 if $state =~ /^on$/; 2595 $state = 0 if $state =~ /^inactive$/; 2596 $state = 0 if $state =~ /^off$/; 2597 2598 my $type = shift || 'u'; # create limited users by default - fwiw, not sure yet how this will interact with ACLs 2599 2600 my $permstring = shift || 'i'; # default is to inhert permissions from group 2601 2602 my $fname = shift || $username; 2603 my $lname = shift || ''; 2604 my $phone = shift || ''; # not going format-check 2605 2606 my $sth = $dbh->prepare("SELECT user_id FROM users WHERE username=?"); 2607 my $user_id; 2608 2609 # quick check to start to see if we've already got one 2610 $sth->execute($username); 2611 ($user_id) = $sth->fetchrow_array; 2612 2613 return ('FAIL', "User already exists") if $user_id; 2614 2615 # Allow transactions, and raise an exception on errors so we can catch it later. 2616 # Use local to make sure these get "reset" properly on exiting this block 2617 local $dbh->{AutoCommit} = 0; 2618 local $dbh->{RaiseError} = 1; 2619 2620 # Wrap all the SQL in a transaction 2621 eval { 2622 # insert the user... note we set inherited perms by default since 2623 # it's simple and cleans up some other bits of state 2624 my $sth = $dbh->prepare("INSERT INTO users ". 2625 "(group_id,username,password,firstname,lastname,phone,type,status,permission_id,inherit_perm) ". 2626 "VALUES (?,?,?,?,?,?,?,?,(SELECT permission_id FROM permissions WHERE group_id=?),'t')"); 2627 $sth->execute($group,$username,unix_md5_crypt($pass),$fname,$lname,$phone,$type,$state,$group); 2628 2629 # get the ID... 2630 ($user_id) = $dbh->selectrow_array("SELECT currval('users_user_id_seq')"); 2631 2632 # Permissions! Gotta set'em all! 2633 die "Invalid permission string $permstring\n" 2634 if $permstring !~ /^(?: 2635 i # inherit 2636 |c:\d+ # clone 2637 # custom. no, the leading , is not a typo 2638 |C:(?:,(?:group|user|domain|record|location|self)_(?:edit|create|delete|locchg|view))* 2639 )$/x; 2640 # bleh. I'd call another function to do my dirty work, but we're in the middle of a transaction already. 2641 if ($permstring ne 'i') { 2642 # for cloned or custom permissions, we have to create a new permissions entry. 2643 my $clonesrc = $group; 2644 if ($permstring =~ /^c:(\d+)/) { $clonesrc = $1; } 2645 $dbh->do("INSERT INTO permissions ($permlist,user_id) ". 2646 "SELECT $permlist,? FROM permissions WHERE permission_id=". 2647 "(SELECT permission_id FROM permissions WHERE ".($permstring =~ /^c:/ ? 'user' : 'group')."_id=?)", 2648 undef, ($user_id,$clonesrc) ); 2649 $dbh->do("UPDATE users SET permission_id=". 2650 "(SELECT permission_id FROM permissions WHERE user_id=?) ". 2651 "WHERE user_id=?", undef, ($user_id, $user_id) ); 2652 } 2653 if ($permstring =~ /^C:/) { 2654 # finally for custom permissions, we set the passed-in permissions (and unset 2655 # any that might have been brought in by the clone operation above) 2656 my ($permid) = $dbh->selectrow_array("SELECT permission_id FROM permissions WHERE user_id=?", 2657 undef, ($user_id) ); 2658 foreach (@permtypes) { 2659 if ($permstring =~ /,$_/) { 2660 $dbh->do("UPDATE permissions SET $_='t' WHERE permission_id=?", undef, ($permid) ); 2661 } else { 2662 $dbh->do("UPDATE permissions SET $_='f' WHERE permission_id=?", undef, ($permid) ); 2663 } 2664 } 2665 } 2666 2667 $dbh->do("UPDATE users SET inherit_perm='n' WHERE user_id=?", undef, ($user_id) ); 2668 2669 ##fixme: add another table to hold name/email for log table? 2670 2671 _log($dbh, (group_id => $group, entry => "Added user $username ($fname $lname)")); 2672 # once we get here, we should have suceeded. 2673 $dbh->commit; 2674 }; # end eval 2675 2676 if ($@) { 2677 my $msg = $@; 2678 eval { $dbh->rollback; }; 2679 if ($config{log_failures}) { 2680 _log($dbh, (group_id => $group, entry => "Error adding user $username: $msg")); 2681 $dbh->commit; # since we enabled transactions earlier 2682 } 2683 return ('FAIL',"Error adding user $username: $msg"); 2684 } 2685 2686 return ('OK',"User $username ($fname $lname) added"); 2687 } # end addUser 2688 2689 2690 ## DNSDB::getUserCount() 2691 # Get count of users in group 2692 # Takes a database handle and hash containing at least the current group, and optionally: 2693 # - a reference list of secondary groups 2694 # - a filter string 2695 # - a "Starts with" string 2696 sub getUserCount { 2697 my $dbh = shift; 2698 2699 my %args = @_; 2700 2701 my @filterargs; 2702 2703 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 2704 push @filterargs, "^$args{startwith}" if $args{startwith}; 2705 push @filterargs, $args{filter} if $args{filter}; 2706 2707 2708 my $sql = "SELECT count(*) FROM users ". 2709 "WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2710 ($args{startwith} ? " AND username ~* ?" : ''). 2711 ($args{filter} ? " AND username ~* ?" : ''); 2712 my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) ); 2713 $errstr = $dbh->errstr if !$count; 2714 return $count; 2715 } # end getUserCount() 2716 2717 2718 ## DNSDB::getUserList() 2719 # Get list of users 2720 # Takes the same arguments as getUserCount() above, plus optional: 2721 # - sort field 2722 # - sort order 2723 # - offset/return-all-everything flag (defaults to $perpage records) 2724 sub getUserList { 2725 my $dbh = shift; 2726 2727 my %args = @_; 2728 2729 my @filterargs; 2730 2731 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 2732 push @filterargs, "^$args{startwith}" if $args{startwith}; 2733 push @filterargs, $args{filter} if $args{filter}; 2734 2735 # better to request sorts on "simple" names, but it means we need to map it to real columns 2736 my %sortmap = (user => 'u.username', type => 'u.type', group => 'g.group_name', status => 'u.status', 2737 fname => 'fname'); 2738 $args{sortby} = $sortmap{$args{sortby}}; 2739 2740 # protection against bad or missing arguments 2741 $args{sortorder} = 'ASC' if !$args{sortorder}; 2742 $args{sortby} = 'u.username' if !$args{sortby}; 2743 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 2744 2745 my $sql = "SELECT u.user_id, u.username, u.firstname || ' ' || u.lastname AS fname, u.type, g.group_name, u.status ". 2746 "FROM users u ". 2747 "INNER JOIN groups g ON u.group_id=g.group_id ". 2748 "WHERE u.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 2749 ($args{startwith} ? " AND u.username ~* ?" : ''). 2750 ($args{filter} ? " AND u.username ~* ?" : ''). 2751 " AND NOT u.type = 'R' ". 2752 " ORDER BY $args{sortby} $args{sortorder} ". 2753 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); 2754 my $ulist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) ); 2755 $errstr = $dbh->errstr if !$ulist; 2756 return $ulist; 2757 } # end getUserList() 2758 2759 2760 ## DNSDB::getUserDropdown() 2761 # Get a list of usernames for use in a dropdown menu. 2762 # Takes a database handle, current group, and optional "tag this as selected" flag. 2763 # Returns a reference to a list of hashrefs suitable to feeding to HTML::Template 2764 sub getUserDropdown { 2765 my $dbh = shift; 2766 my $grp = shift; 2767 my $sel = shift || 0; 2768 2769 my $sth = $dbh->prepare("SELECT username,user_id FROM users WHERE group_id=?"); 2770 $sth->execute($grp); 2771 2772 my @userlist; 2773 while (my ($username,$uid) = $sth->fetchrow_array) { 2774 my %row = ( 2775 username => $username, 2776 uid => $uid, 2777 selected => ($sel == $uid ? 1 : 0) 2778 ); 2779 push @userlist, \%row; 2780 } 2781 return \@userlist; 2782 } # end getUserDropdown() 2783 2784 2785 ## DNSDB::checkUser() 2786 # Check user/pass combo on login 2787 sub checkUser { 2788 my $dbh = shift; 2789 my $user = shift; 2790 my $inpass = shift; 2791 2792 my $sth = $dbh->prepare("SELECT user_id,group_id,password,firstname,lastname FROM users WHERE username=?"); 2793 $sth->execute($user); 2794 my ($uid,$gid,$pass,$fname,$lname) = $sth->fetchrow_array; 2795 my $loginfailed = 1 if !defined($uid); 2796 2797 if ($pass =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) { 2798 $loginfailed = 1 if $pass ne unix_md5_crypt($inpass,$1); 2799 } else { 2800 $loginfailed = 1 if $pass ne $inpass; 2801 } 2802 2803 # nnnngggg 2804 return ($uid, $gid); 2805 } # end checkUser 2806 2807 2808 ## DNSDB:: updateUser() 2809 # Update general data about user 2810 sub updateUser { 2811 my $dbh = shift; 2812 2813 ##fixme: tweak calling convention so that we can update any given bit of data 2814 my $uid = shift; 2815 my $username = shift; 2816 my $group = shift; 2817 my $pass = shift; 2818 my $state = shift; 2819 my $type = shift || 'u'; 2820 my $fname = shift || $username; 2821 my $lname = shift || ''; 2822 my $phone = shift || ''; # not going format-check 2823 2824 my $resultmsg = ''; 2825 2826 # Munge in some alternate state values 2827 $state = 1 if $state =~ /^active$/; 2828 $state = 1 if $state =~ /^on$/; 2829 $state = 0 if $state =~ /^inactive$/; 2830 $state = 0 if $state =~ /^off$/; 2831 2832 # Allow transactions, and raise an exception on errors so we can catch it later. 2833 # Use local to make sure these get "reset" properly on exiting this block 2834 local $dbh->{AutoCommit} = 0; 2835 local $dbh->{RaiseError} = 1; 2836 2837 my $sth; 2838 2839 # Password can be left blank; if so we assume there's one on file. 2840 # Actual blank passwords are bad, mm'kay? 2841 if (!$pass) { 2842 ($pass) = $dbh->selectrow_array("SELECT password FROM users WHERE user_id=?", undef, ($uid)); 2843 } else { 2844 $pass = unix_md5_crypt($pass); 2845 } 2846 2847 eval { 2848 $dbh->do("UPDATE users SET username=?, password=?, firstname=?, lastname=?, phone=?, type=?, status=?". 2849 " WHERE user_id=?", undef, ($username, $pass, $fname, $lname, $phone, $type, $state, $uid)); 2850 $resultmsg = "Updated user info for $username ($fname $lname)"; 2851 _log($dbh, group_id => $group, entry => $resultmsg); 2852 $dbh->commit; 2853 }; 2854 if ($@) { 2855 my $msg = $@; 2856 eval { $dbh->rollback; }; 2857 if ($config{log_failures}) { 2858 _log($dbh, (group_id => $group, entry => "Error updating user $username: $msg")); 2859 $dbh->commit; # since we enabled transactions earlier 2860 } 2861 return ('FAIL',"Error updating user $username: $msg"); 2862 } 2863 2864 return ('OK',$resultmsg); 2865 } # end updateUser() 2866 2867 2868 ## DNSDB::delUser() 2869 # Delete a user. 2870 # Takes a database handle and user ID 2871 # Returns a success/failure code and matching message 2872 sub delUser { 2873 my $dbh = shift; 2874 my $userid = shift; 2875 2876 return ('FAIL',"Bad userid") if !defined($userid); 2877 2878 my $userdata = getUserData($dbh, $userid); 2879 2880 # Allow transactions, and raise an exception on errors so we can catch it later. 2881 # Use local to make sure these get "reset" properly on exiting this block 2882 local $dbh->{AutoCommit} = 0; 2883 local $dbh->{RaiseError} = 1; 2884 2885 eval { 2886 $dbh->do("DELETE FROM users WHERE user_id=?", undef, ($userid)); 2887 _log($dbh, (group_id => $userdata->{group_id}, 2888 entry => "Deleted user ID $userid/".$userdata->{username}. 2889 " (".$userdata->{firstname}." ".$userdata->{lastname}.")") ); 2890 $dbh->commit; 2891 }; 2892 if ($@) { 2893 my $msg = $@; 2894 eval { $dbh->rollback; }; 2895 if ($config{log_failures}) { 2896 _log($dbh, (group_id => $userdata->{group_id}, entry => "Error deleting user ID ". 2897 "$userid/".$userdata->{username}.": $msg") ); 2898 $dbh->commit; 2899 } 2900 return ('FAIL',"Error deleting user $userid/".$userdata->{username}.": $msg"); 2901 } 2902 2903 return ('OK',"Deleted user ".$userdata->{username}." (".$userdata->{firstname}." ".$userdata->{lastname}.")"); 2904 } # end delUser 2905 2906 2907 ## DNSDB::userFullName() 2908 # Return a pretty string! 2909 # Takes a user_id and optional printf-ish string to indicate which pieces where: 2910 # %u for the username 2911 # %f for the first name 2912 # %l for the last name 2913 # All other text in the passed string will be left as-is. 2914 ##fixme: need a "smart" option too, so that missing/null/blank first/last names don't give funky output 2915 sub userFullName { 2916 $errstr = ''; 2917 my $dbh = shift; 2918 my $userid = shift; 2919 my $fullformat = shift || '%f %l (%u)'; 2920 my $sth = $dbh->prepare("select username,firstname,lastname from users where user_id=?"); 2921 $sth->execute($userid); 2922 my ($uname,$fname,$lname) = $sth->fetchrow_array(); 2923 $errstr = $DBI::errstr if !$uname; 2924 2925 $fullformat =~ s/\%u/$uname/g; 2926 $fullformat =~ s/\%f/$fname/g; 2927 $fullformat =~ s/\%l/$lname/g; 2928 2929 return $fullformat; 2930 } # end userFullName 2931 2932 2933 ## DNSDB::userStatus() 2934 # Sets and/or returns a user's status 2935 # Takes a database handle, user ID and optionally a status argument 2936 # Returns undef on errors. 2937 sub userStatus { 2938 my $dbh = shift; 2939 my $id = shift; 2940 my $newstatus = shift || 'mu'; 2941 2942 return undef if $id !~ /^\d+$/; 2943 2944 my $userdata = getUserData($dbh, $id); 2945 2946 # Allow transactions, and raise an exception on errors so we can catch it later. 2947 # Use local to make sure these get "reset" properly on exiting this block 2948 local $dbh->{AutoCommit} = 0; 2949 local $dbh->{RaiseError} = 1; 2950 2951 if ($newstatus ne 'mu') { 2952 # ooo, fun! let's see what we were passed for status 2953 eval { 2954 $newstatus = 0 if $newstatus eq 'useroff'; 2955 $newstatus = 1 if $newstatus eq 'useron'; 2956 $dbh->do("UPDATE users SET status=? WHERE user_id=?", undef, ($newstatus, $id)); 2957 2958 $resultstr = ($newstatus ? 'Enabled' : 'Disabled')." user ".$userdata->{username}. 2959 " (".$userdata->{firstname}." ".$userdata->{lastname}.")"; 2960 2961 my %loghash; 2962 $loghash{group_id} = parentID($dbh, (id => $id, type => 'user')); 2963 $loghash{entry} = $resultstr; 2964 _log($dbh, %loghash); 2965 2966 $dbh->commit; 2967 }; 2968 if ($@) { 2969 my $msg = $@; 2970 eval { $dbh->rollback; }; 2971 $resultstr = ''; 2972 $errstr = $msg; 2973 ##fixme: failure logging? 2974 return; 2975 } 2976 } 2977 2978 my ($status) = $dbh->selectrow_array("SELECT status FROM users WHERE user_id=?", undef, ($id)); 2979 return $status; 2980 } # end userStatus() 2981 2982 2983 ## DNSDB::getUserData() 2984 # Get misc user data for display 2985 sub getUserData { 2986 my $dbh = shift; 2987 my $uid = shift; 2988 2989 my $sth = $dbh->prepare("SELECT group_id,username,firstname,lastname,phone,type,status,inherit_perm ". 2990 "FROM users WHERE user_id=?"); 2991 $sth->execute($uid); 2992 return $sth->fetchrow_hashref(); 2993 } # end getUserData() 2994 2995 2996 ## DNSDB::addLoc() 2997 # Add a new location. 2998 # Takes a database handle, group ID, short and long description, and a comma-separated 2999 # list of IP addresses. 3000 # Returns ('OK',<location>) on success, ('FAIL',<failmsg>) on failure 3001 sub addLoc { 3002 my $dbh = shift; 3003 my $grp = shift; 3004 my $shdesc = shift; 3005 my $comments = shift; 3006 my $iplist = shift; 3007 3008 # $shdesc gets set to the generated location ID if possible, but these can be de-undefined here. 3009 $comments = '' if !$comments; 3010 $iplist = '' if !$iplist; 3011 3012 my $loc; 3013 3014 # Generate a location ID. This is, by spec, a two-character widget. We'll use [a-z][a-z] 3015 # for now; 676 locations should satisfy all but the largest of the huge networks. 3016 # Not sure whether these are case-sensitive, or what other rules might apply - in any case 3017 # the absolute maximum is 16K (256*256) since it's parsed by tinydns as a two-character field. 3018 3019 # add just after "my $origloc = $loc;": 3020 # # These expand the possible space from 26^2 to 52^2 [* note in testing only 2052 were achieved], 3021 # # and wrap it around. 3022 # # Yes, they skip a couple of possibles. No, I don't care. 3023 # $loc = 'aA' if $loc eq 'zz'; 3024 # $loc = 'Aa' if $loc eq 'zZ'; 3025 # $loc = 'ZA' if $loc eq 'Zz'; 3026 # $loc = 'aa' if $loc eq 'ZZ'; 3027 3028 # Allow transactions, and raise an exception on errors so we can catch it later. 3029 # Use local to make sure these get "reset" properly on exiting this block 3030 local $dbh->{AutoCommit} = 0; 3031 local $dbh->{RaiseError} = 1; 3032 3033 ##fixme: There is probably a far better way to do this. Sequential increments 3034 # are marginally less stupid that pure random generation though, and the existence 3035 # check makes sure we don't stomp on an imported one. 3036 3037 eval { 3038 # Get the "last" location. Note this is the only use for loc_id, because selecting on location Does Funky Things 3039 ($loc) = $dbh->selectrow_array("SELECT location FROM locations ORDER BY loc_id DESC LIMIT 1"); 3040 ($loc) = ($loc =~ /^(..)/); 3041 my $origloc = $loc; 3042 # Make a change... 3043 $loc++; 3044 # ... and keep changing if it exists 3045 while ($dbh->selectrow_array("SELECT count(*) FROM locations WHERE location LIKE ?", undef, ($loc.'%'))) { 3046 $loc++; 3047 ($loc) = ($loc =~ /^(..)/); 3048 die "too many locations in use, can't add another one\n" if $loc eq $origloc; 3049 ##fixme: really need to handle this case faster somehow 3050 #if $loc eq $origloc die "<thwap> bad admin: all locations used, your network is too fragmented"; 3051 } 3052 # And now we should have a unique location. tinydns fundamentally limits the 3053 # number of these but there's no doc on what characters are valid. 3054 $shdesc = $loc if !$shdesc; 3055 $dbh->do("INSERT INTO locations (location, group_id, iplist, description, comments) VALUES (?,?,?,?,?)", 3056 undef, ($loc, $grp, $iplist, $shdesc, $comments) ); 3057 _log($dbh, entry => "Added location ($shdesc, '$iplist')"); 3058 $dbh->commit; 3059 }; 3060 if ($@) { 3061 my $msg = $@; 3062 eval { $dbh->rollback; }; 3063 if ($config{log_failures}) { 3064 $shdesc = $loc if !$shdesc; 3065 _log($dbh, (entry => "Failed adding location ($shdesc, '$iplist'): $msg")); 3066 $dbh->commit; 3067 } 3068 return ('FAIL',$msg); 3069 } 3070 3071 return ('OK',$loc); 3072 } # end addLoc() 3073 3074 3075 ## DNSDB::updateLoc() 3076 sub updateLoc { 3077 my $dbh = shift; 3078 my $loc = shift; 3079 my $grp = shift; 3080 my $shdesc = shift; 3081 my $comments = shift; 3082 my $iplist = shift; 3083 3084 $shdesc = '' if !$shdesc; 3085 $comments = '' if !$comments; 3086 $iplist = '' if !$iplist; 3087 3088 # Allow transactions, and raise an exception on errors so we can catch it later. 3089 # Use local to make sure these get "reset" properly on exiting this block 3090 local $dbh->{AutoCommit} = 0; 3091 local $dbh->{RaiseError} = 1; 3092 3093 my $oldloc = getLoc($dbh, $loc); 3094 my $okmsg = "Updated location (".$oldloc->{description}.", '".$oldloc->{iplist}."') to ($shdesc, '$iplist')"; 3095 3096 eval { 3097 $dbh->do("UPDATE locations SET group_id=?,iplist=?,description=?,comments=? WHERE location=?", 3098 undef, ($grp, $iplist, $shdesc, $comments, $loc) ); 3099 _log($dbh, entry => $okmsg); 3100 $dbh->commit; 3101 }; 3102 if ($@) { 3103 my $msg = $@; 3104 eval { $dbh->rollback; }; 3105 if ($config{log_failures}) { 3106 $shdesc = $loc if !$shdesc; 3107 _log($dbh, (entry => "Failed updating location ($shdesc, '$iplist'): $msg")); 3108 $dbh->commit; 3109 } 3110 return ('FAIL',$msg); 3111 } 3112 3113 return ('OK',$okmsg); 3114 } # end updateLoc() 3115 3116 3117 ## DNSDB::delLoc() 3118 sub delLoc {} 3119 3120 3121 ## DNSDB::getLoc() 3122 sub getLoc { 3123 my $dbh = shift; 3124 my $loc = shift; 3125 3126 my $sth = $dbh->prepare("SELECT group_id,iplist,description,comments FROM locations WHERE location=?"); 3127 $sth->execute($loc); 3128 return $sth->fetchrow_hashref(); 3129 } # end getLoc() 3130 3131 3132 ## DNSDB::getLocCount() 3133 # Get count of locations/views 3134 # Takes a database handle and hash containing at least the current group, and optionally: 3135 # - a reference list of secondary groups 3136 # - a filter string 3137 # - a "Starts with" string 3138 sub getLocCount { 3139 my $dbh = shift; 3140 3141 my %args = @_; 3142 3143 my @filterargs; 3144 3145 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 3146 push @filterargs, "^$args{startwith}" if $args{startwith}; 3147 push @filterargs, $args{filter} if $args{filter}; 3148 3149 3150 my $sql = "SELECT count(*) FROM locations ". 3151 "WHERE group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 3152 ($args{startwith} ? " AND description ~* ?" : ''). 3153 ($args{filter} ? " AND description ~* ?" : ''); 3154 my ($count) = $dbh->selectrow_array($sql, undef, (@filterargs) ); 3155 $errstr = $dbh->errstr if !$count; 3156 return $count; 3157 } # end getLocCount() 3158 3159 3160 ## DNSDB::getLocList() 3161 sub getLocList { 3162 my $dbh = shift; 3163 3164 my %args = @_; 3165 3166 my @filterargs; 3167 3168 $args{startwith} = undef if $args{startwith} && $args{startwith} !~ /^(?:[a-z]|0-9)$/; 3169 push @filterargs, "^$args{startwith}" if $args{startwith}; 3170 push @filterargs, $args{filter} if $args{filter}; 3171 3172 # better to request sorts on "simple" names, but it means we need to map it to real columns 3173 # my %sortmap = (user => 'u.username', type => 'u.type', group => 'g.group_name', status => 'u.status', 3174 # fname => 'fname'); 3175 # $args{sortby} = $sortmap{$args{sortby}}; 3176 3177 # protection against bad or missing arguments 3178 $args{sortorder} = 'ASC' if !$args{sortorder}; 3179 $args{sortby} = 'l.description' if !$args{sortby}; 3180 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 3181 3182 my $sql = "SELECT l.location, l.description, l.iplist, g.group_name ". 3183 "FROM locations l ". 3184 "INNER JOIN groups g ON l.group_id=g.group_id ". 3185 "WHERE l.group_id IN ($args{curgroup}".($args{childlist} ? ",$args{childlist}" : '').")". 3186 ($args{startwith} ? " AND l.description ~* ?" : ''). 3187 ($args{filter} ? " AND l.description ~* ?" : ''). 3188 " ORDER BY $args{sortby} $args{sortorder} ". 3189 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); 3190 my $ulist = $dbh->selectall_arrayref($sql, { Slice => {} }, (@filterargs) ); 3191 $errstr = $dbh->errstr if !$ulist; 3192 return $ulist; 3193 } # end getLocList() 3194 3195 3196 ## DNSDB::getLocDropdown() 3197 # Get a list of location names for use in a dropdown menu. 3198 # Takes a database handle, current group, and optional "tag this as selected" flag. 3199 # Returns a reference to a list of hashrefs suitable to feeding to HTML::Template 3200 sub getLocDropdown { 3201 my $dbh = shift; 3202 my $grp = shift; 3203 my $sel = shift || ''; 3204 3205 my $sth = $dbh->prepare(qq( 3206 SELECT description,location FROM locations 3207 WHERE group_id=? 3208 ORDER BY description 3209 ) ); 3210 $sth->execute($grp); 3211 3212 my @loclist; 3213 push @loclist, { locname => "(None/public)", loc => '', selected => ($sel ? 0 : ($sel eq '' ? 1 : 0)) }; 3214 while (my ($locname, $loc) = $sth->fetchrow_array) { 3215 my %row = ( 3216 locname => $locname, 3217 loc => $loc, 3218 selected => ($sel eq $loc ? 1 : 0) 3219 ); 3220 push @loclist, \%row; 3221 } 3222 return \@loclist; 3223 } # end getLocDropdown() 3224 3225 3226 ## DNSDB::getSOA() 3227 # Return all suitable fields from an SOA record in separate elements of a hash 3228 # Takes a database handle, default/live flag, domain/reverse flag, and parent ID 3229 sub getSOA { 3230 $errstr = ''; 3231 my $dbh = shift; 3232 my $def = shift; 3233 my $rev = shift; 3234 my $id = shift; 3235 3236 # (ab)use distance and weight columns to store SOA data? can't for default_rev_records... 3237 # - should really attach serial to the zone parent somewhere 3238 3239 my $sql = "SELECT record_id,host,val,ttl from "._rectable($def,$rev). 3240 " WHERE "._recparent($def,$rev)." = ? AND type=$reverse_typemap{SOA}"; 3241 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) ); 3242 return if !$ret; 3243 ##fixme: stick a flag somewhere if the record doesn't exist. by the API, this is an impossible case, but... 3244 3245 ($ret->{contact},$ret->{prins}) = split /:/, $ret->{host}; 3246 delete $ret->{host}; 3247 ($ret->{refresh},$ret->{retry},$ret->{expire},$ret->{minttl}) = split /:/, $ret->{val}; 3248 delete $ret->{val}; 3249 3250 return $ret; 3251 } # end getSOA() 3252 3253 3254 ## DNSDB::updateSOA() 3255 # Update the specified SOA record 3256 # Takes a database handle, default/live flag, forward/reverse flag, and SOA data hash 3257 # Returns a two-element list with a result code and message 3258 sub updateSOA { 3259 my $dbh = shift; 3260 my $defrec = shift; 3261 my $revrec = shift; 3262 3263 my %soa = @_; 3264 3265 my $oldsoa = getSOA($dbh, $defrec, $revrec, $soa{id}); 3266 3267 my $msg; 3268 my %logdata; 3269 if ($defrec eq 'n') { 3270 $logdata{domain_id} = $soa{id} if $revrec eq 'n'; 3271 $logdata{rdns_id} = $soa{id} if $revrec eq 'y'; 3272 $logdata{group_id} = parentID($dbh, (id => $soa{id}, revrec => $revrec, 3273 type => ($revrec eq 'n' ? 'domain' : 'revzone') ) ); 3274 } else { 3275 $logdata{group_id} = $soa{id}; 3276 } 3277 my $parname = ($defrec eq 'y' ? groupName($dbh, $soa{id}) : 3278 ($revrec eq 'n' ? domainName($dbh, $soa{id}) : revName($dbh, $soa{id})) ); 3279 3280 # Allow transactions, and raise an exception on errors so we can catch it later. 3281 # Use local to make sure these get "reset" properly on exiting this block 3282 local $dbh->{AutoCommit} = 0; 3283 local $dbh->{RaiseError} = 1; 3284 3285 eval { 3286 my $sql = "UPDATE "._rectable($defrec, $revrec)." SET host=?, val=?, ttl=? WHERE record_id=? AND type=6"; 3287 $dbh->do($sql, undef, ("$soa{contact}:$soa{prins}", "$soa{refresh}:$soa{retry}:$soa{expire}:$soa{minttl}", 3288 $soa{ttl}, $oldsoa->{record_id}) ); 3289 $msg = "Updated ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse ' : 'default ') : ''). 3290 "SOA for $parname: ". 3291 "(ns $oldsoa->{prins}, contact $oldsoa->{contact}, refresh $oldsoa->{refresh},". 3292 " retry $oldsoa->{retry}, expire $oldsoa->{expire}, minTTL $oldsoa->{minttl}, TTL $oldsoa->{ttl}) to ". 3293 "(ns $soa{prins}, contact $soa{contact}, refresh $soa{refresh},". 3294 " retry $soa{retry}, expire $soa{expire}, minTTL $soa{minttl}, TTL $soa{ttl})"; 3295 3296 $logdata{entry} = $msg; 3297 _log($dbh, %logdata); 3298 3299 $dbh->commit; 3300 }; 3301 if ($@) { 3302 $msg = $@; 3303 eval { $dbh->rollback; }; 3304 $logdata{entry} = "Error updating ".($defrec eq 'y' ? ($revrec eq 'y' ? 'default reverse zone ' : 'default ') : ''). 3305 "SOA record for $parname: $msg"; 3306 if ($config{log_failures}) { 3307 _log($dbh, %logdata); 3308 $dbh->commit; 3309 } 3310 return ('FAIL', $logdata{entry}); 3311 } else { 3312 return ('OK', $msg); 3313 } 3314 } # end updateSOA() 3315 3316 3317 ## DNSDB::getRecLine() 3318 # Return all data fields for a zone record in separate elements of a hash 3319 # Takes a database handle, default/live flag, forward/reverse flag, and record ID 3320 sub getRecLine { 3321 $errstr = ''; 3322 my $dbh = shift; 3323 my $defrec = shift; 3324 my $revrec = shift; 3325 my $id = shift; 3326 3327 my $sql = "SELECT record_id,host,type,val,ttl,location".($revrec eq 'n' ? ',distance,weight,port' : ''). 3328 (($defrec eq 'y') ? ',group_id FROM ' : ',domain_id,rdns_id FROM '). 3329 _rectable($defrec,$revrec)." WHERE record_id=?"; 3330 my $ret = $dbh->selectrow_hashref($sql, undef, ($id) ); 3331 3332 if ($dbh->err) { 3333 $errstr = $DBI::errstr; 3334 return undef; 3335 } 3336 3337 if (!$ret) { 3338 $errstr = "No such record"; 3339 return undef; 3340 } 3341 3342 # explicitly set a parent id 3343 if ($defrec eq 'y') { 3344 $ret->{parid} = $ret->{group_id}; 3345 } else { 3346 $ret->{parid} = (($revrec eq 'n') ? $ret->{domain_id} : $ret->{rdns_id}); 3347 # and a secondary if we have a custom type that lives in both a forward and reverse zone 3348 $ret->{secid} = (($revrec eq 'y') ? $ret->{domain_id} : $ret->{rdns_id}) if $ret->{type} > 65279; 3349 } 3350 3351 return $ret; 3352 } 3353 3354 3355 ##fixme: should use above (getRecLine()) to get lines for below? 3356 ## DNSDB::getDomRecs() 3357 # Return records for a domain 3358 # Takes a database handle, default/live flag, group/domain ID, start, 3359 # number of records, sort field, and sort order 3360 # Returns a reference to an array of hashes 3361 sub getDomRecs { 3362 $errstr = ''; 3363 my $dbh = shift; 3364 3365 my %args = @_; 3366 3367 my @filterargs; 3368 3369 push @filterargs, $args{filter} if $args{filter}; 3370 3371 # protection against bad or missing arguments 3372 $args{sortorder} = 'ASC' if !$args{sortorder}; 3373 $args{sortby} = 'host' if !$args{sortby} && $args{revrec} eq 'n'; # default sort by host on domain record list 3374 $args{sortby} = 'val' if !$args{sortby} && $args{revrec} eq 'y'; # default sort by IP on revzone record list 3375 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 3376 3377 # sort reverse zones on IP, correctly 3378 # do other fiddling with $args{sortby} while we're at it. 3379 $args{sortby} = "r.$args{sortby}"; 3380 $args{sortby} = 'CAST (r.val AS inet)' 3381 if $args{revrec} eq 'y' && $args{defrec} eq 'n' && $args{sortby} eq 'r.val'; 3382 $args{sortby} = 't.alphaorder' if $args{sortby} eq 'r.type'; 3383 3384 my $sql = "SELECT r.record_id,r.host,r.type,r.val,r.ttl"; 3385 $sql .= ",l.description AS locname" if $args{defrec} eq 'n'; 3386 $sql .= ",r.distance,r.weight,r.port" if $args{revrec} eq 'n'; 3387 $sql .= " FROM "._rectable($args{defrec},$args{revrec})." r "; 3388 3389 # whee! multisort means just passing comma-separated fields in sortby! 3390 my $newsort = ''; 3391 foreach my $sf (split /,/, $order) { 3392 $sf = "r.$sf"; 3393 $sf =~ s/r\.type/t.alphaorder/; 3394 $newsort .= ",$sf"; 3395 } 3396 $newsort =~ s/^,//; 3397 3398 $sql .= "INNER JOIN rectypes t ON r.type=t.val "; # for sorting by type alphabetically 3399 $sql .= "LEFT JOIN locations l ON r.location=l.location " if $args{defrec} eq 'n'; 3400 $sql .= "WHERE "._recparent($args{defrec},$args{revrec})." = ?"; 3401 $sql .= " AND NOT r.type=$reverse_typemap{SOA}"; 3402 $sql .= " AND host ~* ?" if $args{filter}; 3403 $sql .= " ORDER BY $args{sortby} $args{sortorder}"; 3404 # ensure consistent ordering by sorting on record_id too 3405 $sql .= ", record_id $args{sortorder}"; 3406 $sql .= ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); 3407 3408 my @bindvars = ($args{id}); 3409 push @bindvars, $args{filter} if $args{filter}; 3410 3411 my $ret = $dbh->selectall_arrayref($sql, { Slice => {} }, (@bindvars) ); 3412 return $ret; 3413 } # end getDomRecs() 3414 3415 3416 ## DNSDB::getRecCount() 3417 # Return count of non-SOA records in zone (or default records in a group) 3418 # Takes a database handle, default/live flag, reverse/forward flag, group/domain ID, 3419 # and optional filtering modifier 3420 # Returns the count 3421 sub getRecCount { 3422 my $dbh = shift; 3423 my $defrec = shift; 3424 my $revrec = shift; 3425 my $id = shift; 3426 my $filter = shift || ''; 3427 3428 # keep the nasties down, since we can't ?-sub this bit. :/ 3429 # note this is chars allowed in DNS hostnames 3430 $filter =~ s/[^a-zA-Z0-9_.:-]//g; 3431 3432 my @bindvars = ($id); 3433 push @bindvars, $filter if $filter; 3434 my $sql = "SELECT count(*) FROM ". 3435 _rectable($defrec,$revrec). 3436 " WHERE "._recparent($defrec,$revrec)."=? ". 3437 "AND NOT type=$reverse_typemap{SOA}". 3438 ($filter ? " AND host ~* ?" : ''); 3439 my ($count) = $dbh->selectrow_array($sql, undef, (@bindvars) ); 3440 3441 return $count; 3442 3443 } # end getRecCount() 3444 3445 3446 ## DNSDB::addRec() 3447 # Add a new record to a domain or a group's default records 3448 # Takes a database handle, default/live flag, group/domain ID, 3449 # host, type, value, and TTL 3450 # Some types require additional detail: "distance" for MX and SRV, 3451 # and weight/port for SRV 3452 # Returns a status code and detail message in case of error 3453 ##fixme: pass a hash with the record data, not a series of separate values 3454 sub addRec { 3455 $errstr = ''; 3456 my $dbh = shift; 3457 my $defrec = shift; 3458 my $revrec = shift; 3459 my $id = shift; # parent (group_id for defrecs, rdns_id for reverse records, 3460 # domain_id for domain records) 3461 3462 my $host = shift; 3463 my $rectype = shift; # reference so we can coerce it if "+"-types can't find both zones 3464 my $val = shift; 3465 my $ttl = shift; 3466 my $location = shift; 3467 $location = '' if !$location; 3468 3469 # Spaces are evil. 3470 $host =~ s/^\s+//; 3471 $host =~ s/\s+$//; 3472 if ($typemap{$rectype} ne 'TXT') { 3473 # Leading or trailng spaces could be legit in TXT records. 3474 $val =~ s/^\s+//; 3475 $val =~ s/\s+$//; 3476 } 3477 3478 # Validation 3479 my $addr = NetAddr::IP->new($val); 3480 if ($rectype == $reverse_typemap{A}) { 3481 return ('FAIL',$typemap{$rectype}." record must be a valid IPv4 address") 3482 unless $addr && !$addr->{isv6}; 3483 } 3484 if ($rectype == $reverse_typemap{AAAA}) { 3485 return ('FAIL',$typemap{$rectype}." record must be a valid IPv6 address") 3486 unless $addr && $addr->{isv6}; 3487 } 3488 3489 my $domid = 0; 3490 my $revid = 0; 3491 3492 my $retcode = 'OK'; # assume everything will go OK 3493 my $retmsg = ''; 3494 3495 # do simple validation first 3496 return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/; 3497 3498 # Quick check on hostname parts. Note the regex is more forgiving than the error message; 3499 # domain names technically are case-insensitive, and we use printf-like % codes for a couple 3500 # of types. Other things may also be added to validate default records of several flavours. 3501 return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z . _)") 3502 if $defrec eq 'n' && ($revrec eq 'y' ? $$rectype != $reverse_typemap{TXT} : 1) && 3503 $$host !~ /^[0-9a-z_%.-]+$/i; 3504 3505 # Collect these even if we're only doing a simple A record so we can call *any* validation sub 3506 my $dist = shift; 3507 my $weight = shift; 3508 my $port = shift; 3509 3510 my $fields; 3511 my @vallist; 3512 3513 # Call the validation sub for the type requested. 3514 ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, id => $id, 3515 host => $host, rectype => $rectype, val => $val, addr => $addr, 3516 dist => \$dist, port => \$port, weight => \$weight, 3517 fields => \$fields, vallist => \@vallist) ); 3518 3519 return ($retcode,$retmsg) if $retcode eq 'FAIL'; 3520 3521 # Set up database fields and bind parameters 3522 $fields .= "host,type,val,ttl,location,"._recparent($defrec,$revrec); 3523 push @vallist, ($$host,$$rectype,$$val,$ttl,$location,$id); 3524 my $vallen = '?'.(',?'x$#vallist); 3525 3526 # Put together the success log entry. We have to use this horrible kludge 3527 # because domain_id and rdns_id may or may not be present, and if they are, 3528 # they're not at a guaranteed consistent index in the array. wheee! 3529 my %logdata; 3530 my @ftmp = split /,/, $fields; 3531 for (my $i=0; $i <= $#vallist; $i++) { 3532 $logdata{domain_id} = $vallist[$i] if $ftmp[$i] eq 'domain_id'; 3533 $logdata{rdns_id} = $vallist[$i] if $ftmp[$i] eq 'rdns_id'; 3534 } 3535 $logdata{group_id} = $id if $defrec eq 'y'; 3536 $logdata{group_id} = parentID($dbh, 3537 (id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ) 3538 if $defrec eq 'n'; 3539 $logdata{entry} = "Added ".($defrec eq 'y' ? 'default record' : 'record'); 3540 # NS records for revzones get special treatment 3541 if ($revrec eq 'y' && $$rectype == 2) { 3542 $logdata{entry} .= " '$$val $typemap{$$rectype} $$host"; 3543 } else { 3544 $logdata{entry} .= " '$$host $typemap{$$rectype} $$val"; 3545 } 3546 3547 $logdata{entry} .= " [distance $dist]" if $typemap{$$rectype} eq 'MX'; 3548 $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]" 3549 if $typemap{$$rectype} eq 'SRV'; 3550 $logdata{entry} .= "', TTL $ttl, location $location"; 3551 3552 # Allow transactions, and raise an exception on errors so we can catch it later. 3553 # Use local to make sure these get "reset" properly on exiting this block 3554 local $dbh->{AutoCommit} = 0; 3555 local $dbh->{RaiseError} = 1; 3556 3557 eval { 3558 $dbh->do("INSERT INTO "._rectable($defrec, $revrec)." ($fields) VALUES ($vallen)", 3559 undef, @vallist); 3560 _log($dbh, %logdata); 3561 $dbh->commit; 3562 }; 3563 if ($@) { 3564 my $msg = $@; 3565 eval { $dbh->rollback; }; 3566 if ($config{log_failures}) { 3567 $logdata{entry} = "Failed adding ".($defrec eq 'y' ? 'default ' : ''). 3568 "record '$$host $typemap{$$rectype} $$val', TTL $ttl ($msg)"; 3569 _log($dbh, %logdata); 3570 $dbh->commit; 3571 } 3572 return ('FAIL',$msg); 3573 } 3574 3575 $resultstr = $logdata{entry}; 3576 return ($retcode, $retmsg); 3577 3578 } # end addRec() 3579 3580 3581 ## DNSDB::updateRec() 3582 # Update a record 3583 # Takes a database handle, default and reverse flags, record ID, immediate parent ID, and new record data. 3584 # Returns a status code and message 3585 sub updateRec { 3586 $errstr = ''; 3587 3588 my $dbh = shift; 3589 my $defrec = shift; 3590 my $revrec = shift; 3591 my $id = shift; 3592 my $parid = shift; # immediate parent entity that we're descending from to update the record 3593 3594 # all records have these 3595 my $host = shift; 3596 my $hostbk = $$host; # Keep a backup copy of the original, so we can WARN if the update mangles the domain 3597 my $rectype = shift; 3598 my $val = shift; 3599 my $ttl = shift; 3600 my $location = shift; # may be empty/null/undef depending on caller 3601 $location = '' if !$location; 3602 3603 # prep for validation 3604 my $addr = NetAddr::IP->new($$val); 3605 $$host =~ s/\.+$//; # FQDNs ending in . are an internal detail, and really shouldn't be exposed in the UI. 3606 3607 # Spaces are evil. 3608 $host =~ s/^\s+//; 3609 $host =~ s/\s+$//; 3610 if ($typemap{$type} ne 'TXT') { 3611 # Leading or trailng spaces could be legit in TXT records. 3612 $val =~ s/^\s+//; 3613 $val =~ s/\s+$//; 3614 } 3615 3616 my $domid = 0; 3617 my $revid = 0; 3618 3619 my $retcode = 'OK'; # assume everything will go OK 3620 my $retmsg = ''; 3621 3622 # do simple validation first 3623 return ('FAIL', "TTL must be numeric") unless $ttl =~ /^\d+$/; 3624 3625 # Quick check on hostname parts. Note the regex is more forgiving than the error message; 3626 # domain names technically are case-insensitive, and we use printf-like % codes for a couple 3627 # of types. Other things may also be added to validate default records of several flavours. 3628 return ('FAIL', "Hostnames may not contain anything other than (0-9 a-z - . _)") 3629 if $defrec eq 'n' && ($revrec eq 'y' ? $$rectype != $reverse_typemap{TXT} : 1) && 3630 $$host !~ /^[0-9a-z_%.-]+$/i; 3631 3632 # only MX and SRV will use these 3633 my $dist = shift || 0; 3634 my $weight = shift || 0; 3635 my $port = shift || 0; 3636 3637 my $fields; 3638 my @vallist; 3639 3640 # get old record data so we have the right parent ID 3641 # and for logging (eventually) 3642 my $oldrec = getRecLine($dbh, $defrec, $revrec, $id); 3643 3644 # Call the validation sub for the type requested. 3645 # Note the ID to pass here is the *parent*, not the record 3646 ($retcode,$retmsg) = $validators{$$rectype}($dbh, (defrec => $defrec, revrec => $revrec, 3647 id => ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})), 3648 host => $host, rectype => $rectype, val => $val, addr => $addr, 3649 dist => \$dist, port => \$port, weight => \$weight, 3650 fields => \$fields, vallist => \@vallist, 3651 update => $id) ); 3652 3653 return ($retcode,$retmsg) if $retcode eq 'FAIL'; 3654 3655 # Set up database fields and bind parameters. Note only the optional fields 3656 # (distance, weight, port, secondary parent ID) are added in the validation call above 3657 $fields .= "host,type,val,ttl,location,"._recparent($defrec,$revrec); 3658 push @vallist, ($$host,$$rectype,$$val,$ttl,$location, 3659 ($defrec eq 'y' ? $oldrec->{group_id} : ($revrec eq 'n' ? $oldrec->{domain_id} : $oldrec->{rdns_id})) ); 3660 3661 # hack hack PTHUI 3662 # need to forcibly make sure we disassociate a record with a parent it's no longer related to. 3663 # eg, PTR records may not have a domain parent, or A/AAAA records may not have a revzone parent. 3664 # mainly needed for crossover types that got coerced down to "standard" types 3665 if ($defrec eq 'n') { 3666 if ($$rectype == $reverse_typemap{PTR}) { 3667 $fields .= ",domain_id"; 3668 push @vallist, 0; 3669 } 3670 if ($$rectype == $reverse_typemap{A} || $$rectype == $reverse_typemap{AAAA}) { 3671 $fields .= ",rdns_id"; 3672 push @vallist, 0; 3673 } 3674 } 3675 # fix fat-finger-originated record type changes 3676 if ($$rectype == 65285) { 3677 $fields .= ",rdns_id" if $revrec eq 'n'; 3678 $fields .= ",domain_id" if $revrec eq 'y'; 3679 push @vallist, 0; 3680 } 3681 if ($defrec eq 'n') { 3682 $domid = $parid if $revrec eq 'n'; 3683 $revid = $parid if $revrec eq 'y'; 3684 } 3685 3686 # Put together the success log entry. Horrible kludge from addRec() copied as-is since 3687 # we don't know whether the passed arguments or retrieved values for domain_id and rdns_id 3688 # will be maintained (due to "not-in-zone" validation changes) 3689 my %logdata; 3690 $logdata{domain_id} = $domid; 3691 $logdata{rdns_id} = $revid; 3692 my @ftmp = split /,/, $fields; 3693 for (my $i=0; $i <= $#vallist; $i++) { 3694 $logdata{domain_id} = $vallist[$i] if $ftmp[$i] eq 'domain_id'; 3695 $logdata{rdns_id} = $vallist[$i] if $ftmp[$i] eq 'rdns_id'; 3696 } 3697 $logdata{group_id} = $parid if $defrec eq 'y'; 3698 $logdata{group_id} = parentID($dbh, 3699 (id => $parid, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ) 3700 if $defrec eq 'n'; 3701 $logdata{entry} = "Updated ".($defrec eq 'y' ? 'default record' : 'record')." from\n"; 3702 # NS records for revzones get special treatment 3703 if ($revrec eq 'y' && $$rectype == 2) { 3704 $logdata{entry} .= " '$oldrec->{val} $typemap{$oldrec->{type}} $oldrec->{host}"; 3705 } else { 3706 $logdata{entry} .= " '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}"; 3707 } 3708 $logdata{entry} .= " [distance $oldrec->{distance}]" if $typemap{$oldrec->{type}} eq 'MX'; 3709 $logdata{entry} .= " [priority $oldrec->{distance}] [weight $oldrec->{weight}] [port $oldrec->{port}]" 3710 if $typemap{$oldrec->{type}} eq 'SRV'; 3711 $logdata{entry} .= "', TTL $oldrec->{ttl}, location $oldrec->{location}\nto\n"; 3712 # More NS special 3713 if ($revrec eq 'y' && $$rectype == 2) { 3714 $logdata{entry} .= "'$$val $typemap{$$rectype} $$host"; 3715 } else { 3716 $logdata{entry} .= "'$$host $typemap{$$rectype} $$val"; 3717 } 3718 $logdata{entry} .= " [distance $dist]" if $typemap{$$rectype} eq 'MX'; 3719 $logdata{entry} .= " [priority $dist] [weight $weight] [port $port]" if $typemap{$$rectype} eq 'SRV'; 3720 $logdata{entry} .= "', TTL $ttl, location $location"; 3721 3722 local $dbh->{AutoCommit} = 0; 3723 local $dbh->{RaiseError} = 1; 3724 3725 # Fiddle the field list into something suitable for updates 3726 $fields =~ s/,/=?,/g; 3727 $fields .= "=?"; 3728 3729 eval { 3730 $dbh->do("UPDATE "._rectable($defrec,$revrec)." SET $fields WHERE record_id=?", undef, (@vallist, $id) ); 3731 _log($dbh, %logdata); 3732 $dbh->commit; 3733 }; 3734 if ($@) { 3735 my $msg = $@; 3736 eval { $dbh->rollback; }; 3737 if ($config{log_failures}) { 3738 $logdata{entry} = "Failed updating ".($defrec eq 'y' ? 'default ' : ''). 3739 "record '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)"; 3740 _log($dbh, %logdata); 3741 $dbh->commit; 3742 } 3743 return ('FAIL', $msg); 3744 } 3745 3746 $resultstr = $logdata{entry}; 3747 return ($retcode, $retmsg); 3748 } # end updateRec() 3749 3750 3751 ## DNSDB::delRec() 3752 # Delete a record. 3753 sub delRec { 3754 $errstr = ''; 3755 my $dbh = shift; 3756 my $defrec = shift; 3757 my $revrec = shift; 3758 my $id = shift; 3759 3760 my $oldrec = getRecLine($dbh, $defrec, $revrec, $id); 3761 3762 # Allow transactions, and raise an exception on errors so we can catch it later. 3763 # Use local to make sure these get "reset" properly on exiting this block 3764 local $dbh->{AutoCommit} = 0; 3765 local $dbh->{RaiseError} = 1; 3766 3767 # Put together the log entry 3768 my %logdata; 3769 $logdata{domain_id} = $oldrec->{domain_id}; 3770 $logdata{rdns_id} = $oldrec->{rdns_id}; 3771 $logdata{group_id} = $oldrec->{group_id} if $defrec eq 'y'; 3772 $logdata{group_id} = parentID($dbh, 3773 (id => $oldrec->{domain_id}, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ) 3774 if $defrec eq 'n'; 3775 $logdata{entry} = "Deleted ".($defrec eq 'y' ? 'default record ' : 'record '). 3776 "'$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}"; 3777 $logdata{entry} .= " [distance $oldrec->{distance}]" if $typemap{$oldrec->{type}} eq 'MX'; 3778 $logdata{entry} .= " [priority $oldrec->{distance}] [weight $oldrec->{weight}] [port $oldrec->{port}]" 3779 if $typemap{$oldrec->{type}} eq 'SRV'; 3780 $logdata{entry} .= "', TTL $oldrec->{ttl}\n"; 3781 3782 eval { 3783 my $sth = $dbh->do("DELETE FROM "._rectable($defrec,$revrec)." WHERE record_id=?", undef, ($id)); 3784 _log($dbh, %logdata); 3785 $dbh->commit; 3786 }; 3787 if ($@) { 3788 my $msg = $@; 3789 eval { $dbh->rollback; }; 3790 if ($config{log_failures}) { 3791 $logdata{entry} = "Error deleting ".($defrec eq 'y' ? 'default record' : 'record'). 3792 " '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl} ($msg)"; 3793 _log($dbh, %logdata); 3794 $dbh->commit; 3795 } 3796 return ('FAIL', $msg); 3797 } 3798 3799 return ('OK',$logdata{entry}); 3800 } # end delRec() 3801 3802 3803 ## DNSDB::getLogCount() 3804 # Get a count of log entries 3805 # Takes a database handle and a hash containing at least: 3806 # - Entity ID and entity type as the primary log "slice" 3807 sub getLogCount { 3808 my $dbh = shift; 3809 3810 my %args = @_; 3811 3812 my @filterargs; 3813 ##fixme: which fields do we want to filter on? 3814 # push @filterargs, 3815 3816 $errstr = 'Missing primary parent ID and/or type'; 3817 # fail early if we don't have a "prime" ID to look for log entries for 3818 return if !$args{id}; 3819 3820 # or if the prime id type is missing or invalid 3821 return if !$args{logtype}; 3822 $args{logtype} = 'revzone' if $args{logtype} eq 'rdns'; # hack pthui 3823 $args{logtype} = 'domain' if $args{logtype} eq 'dom'; # hack pthui 3824 return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user'); 3825 3826 $args{logtype} = 'revzone' if $args{logtype} eq 'rdns'; # hack pthui 3827 3828 my $sql = "SELECT count(*) FROM log ". 3829 "WHERE $id_col{$args{logtype}}=?". 3830 ($args{filter} ? " AND entry ~* ?" : ''); 3831 my ($count) = $dbh->selectrow_array($sql, undef, ($args{id}, @filterargs) ); 3832 $errstr = $dbh->errstr if !$count; 3833 return $count; 3834 } # end getLogCount() 3835 3836 3837 ## DNSDB::getLogEntries() 3838 # Get a list of log entries 3839 # Takes arguments as with getLogCount() above, plus optional: 3840 # - sort field 3841 # - sort order 3842 # - offset for pagination 3843 sub getLogEntries { 3844 my $dbh = shift; 3845 3846 my %args = @_; 3847 3848 my @filterargs; 3849 3850 # fail early if we don't have a "prime" ID to look for log entries for 3851 return if !$args{id}; 3852 3853 # or if the prime id type is missing or invalid 3854 return if !$args{logtype}; 3855 $args{logtype} = 'revzone' if $args{logtype} eq 'rdns'; # hack pthui 3856 $args{logtype} = 'domain' if $args{logtype} eq 'dom'; # hack pthui 3857 return if !grep /^$args{logtype}$/, ('group', 'domain', 'revzone', 'user'); 3858 3859 # Sorting defaults 3860 $args{sortby} = 'stamp' if !$args{sortby}; 3861 $args{sortorder} = 'DESC' if !$args{sortorder}; 3862 $args{offset} = 0 if !$args{offset} || $args{offset} !~ /^(?:all|\d+)$/; 3863 3864 my %sortmap = (fname => 'name', username => 'email', entry => 'entry', stamp => 'stamp'); 3865 $args{sortby} = $sortmap{$args{sortby}}; 3866 3867 my $sql = "SELECT user_id AS userid, email AS useremail, name AS userfname, entry AS logentry, ". 3868 "date_trunc('second',stamp) AS logtime ". 3869 "FROM log ". 3870 "WHERE $id_col{$args{logtype}}=?". 3871 ($args{filter} ? " AND entry ~* ?" : ''). 3872 " ORDER BY $args{sortby} $args{sortorder}, log_id $args{sortorder}". 3873 ($args{offset} eq 'all' ? '' : " LIMIT $config{perpage} OFFSET ".$args{offset}*$config{perpage}); 3874 my $loglist = $dbh->selectall_arrayref($sql, { Slice => {} }, ($args{id}, @filterargs) ); 3875 $errstr = $dbh->errstr if !$loglist; 3876 return $loglist; 3877 } # end getLogEntries() 3878 2344 3879 2345 3880 ## DNSDB::getTypelist() … … 2517 4052 2518 4053 2519 ## DNSDB:: domStatus()2520 # Sets and/or returns a domain's status2521 # Takes a database handle, domain IDand optionally a status argument2522 # Returns undef on errors.2523 sub domStatus {4054 ## DNSDB::zoneStatus() 4055 # Returns and optionally sets a zone's status 4056 # Takes a database handle, domain/revzone ID, forward/reverse flag, and optionally a status argument 4057 # Returns status, or undef on errors. 4058 sub zoneStatus { 2524 4059 my $dbh = shift; 2525 4060 my $id = shift; 2526 my $newstatus = shift; 4061 my $revrec = shift; 4062 my $newstatus = shift || 'mu'; 2527 4063 2528 4064 return undef if $id !~ /^\d+$/; 2529 4065 2530 my $sth; 2531 2532 # ooo, fun! let's see what we were passed for status 2533 if ($newstatus) { 2534 $sth = $dbh->prepare("update domains set status=? where domain_id=?"); 2535 # ass-u-me caller knows what's going on in full 2536 if ($newstatus =~ /^[01]$/) { # only two valid for now. 2537 $sth->execute($newstatus,$id); 2538 } elsif ($newstatus =~ /^domo(?:n|ff)$/) { 2539 $sth->execute(($newstatus eq 'domon' ? 1 : 0),$id); 2540 } 2541 } 2542 2543 $sth = $dbh->prepare("select status from domains where domain_id=?"); 2544 $sth->execute($id); 2545 my ($status) = $sth->fetchrow_array; 4066 # Allow transactions, and raise an exception on errors so we can catch it later. 4067 # Use local to make sure these get "reset" properly on exiting this block 4068 local $dbh->{AutoCommit} = 0; 4069 local $dbh->{RaiseError} = 1; 4070 4071 if ($newstatus ne 'mu') { 4072 # ooo, fun! let's see what we were passed for status 4073 eval { 4074 $newstatus = 0 if $newstatus eq 'domoff'; 4075 $newstatus = 1 if $newstatus eq 'domon'; 4076 $dbh->do("UPDATE ".($revrec eq 'n' ? 'domains' : 'revzones')." SET status=? WHERE ". 4077 ($revrec eq 'n' ? 'domain_id' : 'rdns_id')."=?", undef, ($newstatus,$id) ); 4078 4079 ##fixme switch to more consise "Enabled <domain"/"Disabled <domain>" as with users? 4080 $resultstr = "Changed ".($revrec eq 'n' ? domainName($dbh, $id) : revName($dbh, $id)). 4081 " state to ".($newstatus ? 'active' : 'inactive'); 4082 4083 my %loghash; 4084 $loghash{domain_id} = $id if $revrec eq 'n'; 4085 $loghash{rdns_id} = $id if $revrec eq 'y'; 4086 $loghash{group_id} = parentID($dbh, 4087 (id => $id, type => ($revrec eq 'n' ? 'domain' : 'revzone'), revrec => $revrec) ); 4088 $loghash{entry} = $resultstr; 4089 _log($dbh, %loghash); 4090 4091 $dbh->commit; 4092 }; 4093 if ($@) { 4094 my $msg = $@; 4095 eval { $dbh->rollback; }; 4096 $resultstr = ''; 4097 $errstr = $msg; 4098 return; 4099 } 4100 } 4101 4102 my ($status) = $dbh->selectrow_array("SELECT status FROM ". 4103 ($revrec eq 'n' ? "domains WHERE domain_id=?" : "revzones WHERE rdns_id=?"), 4104 undef, ($id) ); 2546 4105 return $status; 2547 } # end domStatus()4106 } # end zoneStatus() 2548 4107 2549 4108 … … 2561 4120 my $dbh = shift; 2562 4121 my $ifrom_in = shift; 2563 my $ domain= shift;4122 my $zone = shift; 2564 4123 my $group = shift; 2565 4124 my $status = shift; … … 2569 4128 my $newttl = shift; 2570 4129 4130 my $merge = shift || 0; # do we attempt to merge A/AAAA and PTR records whenever possible? 4131 # do we overload this with the fixme below? 2571 4132 ##fixme: add mode to delete&replace, merge+overwrite, merge new? 2572 4133 … … 2577 4138 my $ifrom; 2578 4139 4140 my $rev = 'n'; 4141 my $code = 'OK'; 4142 my $msg = 'foobar?'; 4143 2579 4144 # choke on possible bad setting in ifrom 2580 4145 # IPv4 and v6, and valid hostnames! … … 2583 4148 unless ($ifrom) = ($ifrom_in =~ /^([0-9a-f\:.]+|[0-9a-z_.-]+)$/i); 2584 4149 4150 my $errmsg; 4151 4152 my $zone_id; 4153 my $domain_id = 0; 4154 my $rdns_id = 0; 4155 my $cidr; 4156 4157 # magic happens! detect if we're importing a domain or a reverse zone 4158 # while we're at it, figure out what the CIDR netblock is (if we got a .arpa) 4159 # or what the formal .arpa zone is (if we got a CIDR netblock) 4160 # Handles sub-octet v4 zones in the format specified in the Cricket Book, 2nd Ed, p217-218 4161 4162 if ($zone =~ m{(?:\.arpa\.?|/\d+)$}) { 4163 # we seem to have a reverse zone 4164 $rev = 'y'; 4165 4166 if ($zone =~ /\.arpa\.?$/) { 4167 # we have a formal reverse zone. call _zone2cidr and get the CIDR block. 4168 ($code,$msg) = _zone2cidr($zone); 4169 return ($code, $msg) if $code eq 'FAIL'; 4170 $cidr = $msg; 4171 } elsif ($zone =~ m|^[\d.]+/\d+$|) { 4172 # v4 revzone, CIDR netblock 4173 $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block"); 4174 $zone = _ZONE($cidr, 'ZONE.in-addr.arpa', 'r', '.'); 4175 } elsif ($zone =~ m|^[a-fA-F\d:]+/\d+$|) { 4176 # v6 revzone, CIDR netblock 4177 $cidr = NetAddr::IP->new($zone) or return ('FAIL',"$zone is not a valid CIDR block"); 4178 return ('FAIL', "$zone is not a nibble-aligned block") if $cidr->masklen % 4 != 0; 4179 $zone = _ZONE($cidr, 'ZONE.ip6.arpa', 'r', '.'); 4180 } else { 4181 # there is. no. else! 4182 return ('FAIL', "Unknown zone name format"); 4183 } 4184 4185 # quick check to start to see if we've already got one 4186 4187 ($zone_id) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet=?", 4188 undef, ("$cidr")); 4189 $rdns_id = $zone_id; 4190 } else { 4191 # default to domain 4192 ($zone_id) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)", 4193 undef, ($zone)); 4194 $domain_id = $zone_id; 4195 } 4196 4197 return ('FAIL', ($rev eq 'n' ? 'Domain' : 'Reverse zone')." already exists") if $zone_id; 4198 4199 # little local utility sub to swap $val and $host for revzone records. 4200 sub _revswap { 4201 my $rechost = shift; 4202 my $recdata = shift; 4203 4204 if ($rechost =~ /\.in-addr\.arpa\.?$/) { 4205 $rechost =~ s/\.in-addr\.arpa\.?$//; 4206 $rechost = join '.', reverse split /\./, $rechost; 4207 } else { 4208 $rechost =~ s/\.ip6\.arpa\.?$//; 4209 my @nibs = reverse split /\./, $rechost; 4210 $rechost = ''; 4211 my $nc; 4212 foreach (@nibs) { 4213 $rechost.= $_; 4214 $rechost .= ":" if ++$nc % 4 == 0 && $nc < 32; 4215 } 4216 $rechost .= ":" if $nc < 32 && $rechost !~ /\*$/; # close netblock records? 4217 ##fixme: there's a case that ends up with a partial entry here: 4218 # ip:add:re:ss:: 4219 # can't reproduce after letting it sit overnight after discovery. :( 4220 #print "$rechost\n"; 4221 # canonicalize with NetAddr::IP 4222 $rechost = NetAddr::IP->new($rechost)->addr unless $rechost =~ /\*$/; 4223 } 4224 return ($recdata,$rechost) 4225 } 4226 4227 2585 4228 # Allow transactions, and raise an exception on errors so we can catch it later. 2586 4229 # Use local to make sure these get "reset" properly on exiting this block … … 2588 4231 local $dbh->{RaiseError} = 1; 2589 4232 2590 my $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?"); 2591 my $dom_id; 2592 2593 # quick check to start to see if we've already got one 2594 $sth->execute($domain); 2595 ($dom_id) = $sth->fetchrow_array; 2596 2597 return ('FAIL', "Domain already exists") if $dom_id; 2598 4233 my $sth; 2599 4234 eval { 2600 # can't do this, can't nest transactions. sigh. 2601 #my ($dcode, $dmsg) = addDomain(dbh, domain, group, status); 2602 4235 4236 if ($rev eq 'n') { 2603 4237 ##fixme: serial 2604 my $sth = $dbh->prepare("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)"); 2605 $sth->execute($domain,$group,$status); 4238 $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,?,?)", undef, ($zone,$group,$status) ); 4239 # get domain id so we can do the records 4240 ($zone_id) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')"); 4241 $domain_id = $zone_id; 4242 _log($dbh, (group_id => $group, domain_id => $domain_id, 4243 entry => "[Added ".($status ? 'active' : 'inactive')." domain $zone via AXFR]") ); 4244 } else { 4245 ##fixme: serial 4246 $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,?,?)", undef, ($cidr,$group,$status) ); 4247 # get revzone id so we can do the records 4248 ($zone_id) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); 4249 $rdns_id = $zone_id; 4250 _log($dbh, (group_id => $group, rdns_id => $rdns_id, 4251 entry => "[Added ".($status ? 'active' : 'inactive')." reverse zone $cidr via AXFR]") ); 4252 } 2606 4253 2607 4254 ## bizarre DBI<->Net::DNS interaction bug: … … 2610 4257 ## caused a commit instead of barfing 2611 4258 2612 # get domain id so we can do the records2613 $sth = $dbh->prepare("SELECT domain_id FROM domains WHERE domain=?");2614 $sth->execute($domain);2615 ($dom_id) = $sth->fetchrow_array();2616 2617 4259 my $res = Net::DNS::Resolver->new; 2618 4260 $res->nameservers($ifrom); 2619 $res->axfr_start($ domain)4261 $res->axfr_start($zone) 2620 4262 or die "Couldn't begin AXFR\n"; 2621 4263 4264 $sth = $dbh->prepare("INSERT INTO records (domain_id,rdns_id,host,type,val,distance,weight,port,ttl)". 4265 " VALUES (?,?,?,?,?,?,?,?,?)"); 4266 4267 # Stash info about sub-octet v4 revzones here so we don't have 4268 # to store the CNAMEs used to delegate a suboctet zone 4269 # $suboct{zone}{ns}[] -> array of nameservers 4270 # $suboct{zone}{cname}[] -> array of extant CNAMEs (Just In Case someone did something bizarre) 4271 ## commented pending actual use of this data. for now, we'll just 4272 ## auto-(re)create the CNAMEs in revzones on export 4273 # my %suboct; 4274 2622 4275 while (my $rr = $res->axfr_next()) { 4276 4277 my $val; 4278 my $distance = 0; 4279 my $weight = 0; 4280 my $port = 0; 4281 my $logfrag = ''; 4282 2623 4283 my $type = $rr->type; 2624 4284 my $ttl = ($newttl ? $newttl : $rr->ttl); # allow force-override TTLs 2625 2626 my $sql = "INSERT INTO records (domain_id,host,type,ttl,val"; 2627 my $vallen = "?,?,?,?,?"; 4285 my $host = $rr->name; 2628 4286 2629 4287 $soaflag = 1 if $type eq 'SOA'; 2630 4288 $nsflag = 1 if $type eq 'NS'; 2631 2632 my @vallist = ($dom_id, $rr->name, $reverse_typemap{$type}, $ttl);2633 4289 2634 4290 # "Primary" types: … … 2636 4292 # maybe KEY 2637 4293 4294 # BIND supports: 4295 # [standard] 4296 # A AAAA CNAME MX NS PTR SOA TXT 4297 # [variously experimental, obsolete, or obscure] 4298 # HINFO MB(ex) MD(ob) MF(ob) MG(ex) MINFO(ex) MR(ex) NULL WKS AFSDB(ex) ISDN(ex) RP(ex) RT(ex) X25(ex) PX 4299 # ... if one can ever find the right magic to format them correctly 4300 4301 # Net::DNS supports: 4302 # RRSIG SIG NSAP NS NIMLOC NAPTR MX MR MINFO MG MB LOC ISDN IPSECKEY HINFO 4303 # EID DNAME CNAME CERT APL AFSDB AAAA A DS NXT NSEC3PARAM NSEC3 NSEC KEY 4304 # DNSKEY DLV X25 TXT TSIG TKEY SSHFP SRV SPF SOA RT RP PX PTR NULL APL::AplItem 4305 2638 4306 # nasty big ugly case-like thing here, since we have to do *some* different 2639 4307 # processing depending on the record. le sigh. … … 2642 4310 2643 4311 if ($type eq 'A') { 2644 push @vallist,$rr->address;4312 $val = $rr->address; 2645 4313 } elsif ($type eq 'NS') { 2646 4314 # hmm. should we warn here if subdomain NS'es are left alone? 2647 next if ($rwns && ($rr->name eq $domain)); 2648 push @vallist, $rr->nsdname; 4315 next if ($rwns && ($rr->name eq $zone)); 4316 if ($rev eq 'y') { 4317 # revzones have records more or less reversed from forward zones. 4318 my ($tmpcode,$tmpmsg) = _zone2cidr($host); 4319 die "Error converting NS record: $tmpmsg\n" if $tmpcode eq 'FAIL'; # hmm. may not make sense... 4320 $val = "$tmpmsg"; 4321 $host = $rr->nsdname; 4322 $logfrag = "Added record '$val $type $host', TTL $ttl"; 4323 # Tag and preserve. For now this is commented for a no-op, but we have Ideas for 4324 # another custom storage type ("DELEGATE") that will use these subzone-delegation records 4325 #if ($val ne "$cidr") { 4326 # push @{$suboct{$val}{ns}}, $host; 4327 #} 4328 } else { 4329 $val = $rr->nsdname; 4330 } 2649 4331 $nsflag = 1; 2650 4332 } elsif ($type eq 'CNAME') { 2651 push @vallist, $rr->cname; 4333 if ($rev eq 'y') { 4334 # hmm. do we even want to bother with storing these at this level? Sub-octet delegation 4335 # by CNAME is essentially a record-publication hack, and we want to just represent the 4336 # "true" logical intentions as far down the stack as we can from the UI. 4337 ($host,$val) = _revswap($host,$rr->cname); 4338 $logfrag = "Added record '$val $type $host', TTL $ttl"; 4339 # Tag and preserve in case we want to commit them as-is later, but mostly we don't care. 4340 # Commented pending actually doing something with possibly new type DELEGATE 4341 #my $tmprev = $host; 4342 #$tmprev =~ s/^\d+\.//; 4343 #($code,$tmprev) = _zone2cidr($tmprev); 4344 #push @{$suboct{"$tmprev"}{cname}}, $val; 4345 # Silently skip CNAMEs in revzones. 4346 next; 4347 } else { 4348 $val = $rr->cname; 4349 } 2652 4350 } elsif ($type eq 'SOA') { 2653 4351 next if $rwsoa; 2654 $ vallist[1] = $rr->mname.":".$rr->rname;2655 push @vallist, ($rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum);4352 $host = $rr->rname.":".$rr->mname; 4353 $val = $rr->refresh.":".$rr->retry.":".$rr->expire.":".$rr->minimum; 2656 4354 $soaflag = 1; 2657 4355 } elsif ($type eq 'PTR') { 2658 push @vallist, $rr->ptrdname; 4356 ($host,$val) = _revswap($host,$rr->ptrdname); 4357 $logfrag = "Added record '$val $type $host', TTL $ttl"; 2659 4358 # hmm. PTR records should not be in forward zones. 2660 4359 } elsif ($type eq 'MX') { 2661 $sql .= ",distance"; 2662 $vallen .= ",?"; 2663 push @vallist, $rr->exchange; 2664 push @vallist, $rr->preference; 4360 $val = $rr->exchange; 4361 $distance = $rr->preference; 2665 4362 } elsif ($type eq 'TXT') { 2666 4363 ##fixme: Net::DNS docs say this should be deprecated for rdatastr() or char_str_list(), 2667 4364 ## but don't really seem enthusiastic about it. 2668 my $rrdata = $rr->txtdata; 2669 push @vallist, $rrdata; 4365 #print "should use rdatastr:\n\t".$rr->rdatastr."\n or char_str_list:\n\t".join(' ',$rr->char_str_list())."\n"; 4366 # rdatastr returns a BIND-targetted logical string, including opening and closing quotes 4367 # char_str_list returns a list of the individual string fragments in the record 4368 # txtdata returns the more useful all-in-one form (since we want to push such protocol 4369 # details as far down the stack as we can) 4370 # NB: this may turn out to be more troublesome if we ever have need of >512-byte TXT records. 4371 if ($rev eq 'y') { 4372 ($host,$val) = _revswap($host,$rr->txtdata); 4373 $logfrag = "Added record '$val $type $host', TTL $ttl"; 4374 } else { 4375 $val = $rr->txtdata; 4376 } 2670 4377 } elsif ($type eq 'SPF') { 2671 4378 ##fixme: and the same caveat here, since it is apparently a clone of ::TXT 2672 my $rrdata = $rr->txtdata; 2673 push @vallist, $rrdata; 4379 $val = $rr->txtdata; 2674 4380 } elsif ($type eq 'AAAA') { 2675 push @vallist,$rr->address;4381 $val = $rr->address; 2676 4382 } elsif ($type eq 'SRV') { 2677 $sql .= ",distance,weight,port" if $type eq 'SRV'; 2678 $vallen .= ",?,?,?" if $type eq 'SRV'; 2679 push @vallist, $rr->target; 2680 push @vallist, $rr->priority; 2681 push @vallist, $rr->weight; 2682 push @vallist, $rr->port; 4383 $val = $rr->target; 4384 $distance = $rr->priority; 4385 $weight = $rr->weight; 4386 $port = $rr->port; 2683 4387 } elsif ($type eq 'KEY') { 2684 4388 # we don't actually know what to do with these... 2685 push @vallist, ($rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname);4389 $val = $rr->flags.":".$rr->protocol.":".$rr->algorithm.":".$rr->key.":".$rr->keytag.":".$rr->privatekeyname; 2686 4390 } else { 2687 my $rrdata = $rr->rdatastr; 2688 push @vallist, $rrdata; 4391 $val = $rr->rdatastr; 2689 4392 # Finding a different record type is not fatal.... just problematic. 2690 4393 # We may not be able to export it correctly. … … 2692 4395 } 2693 4396 2694 # BIND supports: 2695 # A CNAME HINFO MB(ex) MD(ob) MF(ob) MG(ex) MINFO(ex) MR(ex) MX NS NULL 2696 # PTR SOA TXT WKS AFSDB(ex) ISDN(ex) RP(ex) RT(ex) X25(ex) PX 2697 # ... if one can ever find the right magic to format them correctly 2698 2699 # Net::DNS supports: 2700 # RRSIG SIG NSAP NS NIMLOC NAPTR MX MR MINFO MG MB LOC ISDN IPSECKEY HINFO 2701 # EID DNAME CNAME CERT APL AFSDB AAAA A DS NXT NSEC3PARAM NSEC3 NSEC KEY 2702 # DNSKEY DLV X25 TXT TSIG TKEY SSHFP SRV SPF SOA RT RP PX PTR NULL APL::AplItem 2703 2704 $sth = $dbh->prepare($sql.") VALUES (".$vallen.")") or die "problem preparing record insert SQL\n"; 2705 $sth->execute(@vallist) or die "failed to insert ".$rr->string.": ".$sth->errstr."\n"; 4397 my $logentry = "[AXFR ".($rev eq 'n' ? $zone : $cidr)."] "; 4398 4399 if ($merge) { 4400 if ($rev eq 'n') { 4401 # importing a domain; we have A and AAAA records that could be merged with matching PTR records 4402 my $etype; 4403 my ($erdns,$erid,$ettl) = $dbh->selectrow_array("SELECT rdns_id,record_id,ttl FROM records ". 4404 "WHERE host=? AND val=? AND type=12", 4405 undef, ($host, $val) ); 4406 if ($erid) { 4407 if ($type eq 'A') { # PTR -> A+PTR 4408 $etype = 65280; 4409 $logentry .= "Merged A record with existing PTR record '$host A+PTR $val', TTL $ettl"; 4410 } 4411 if ($type eq 'AAAA') { # PTR -> AAAA+PTR 4412 $etype = 65281; 4413 $logentry .= "Merged AAAA record with existing PTR record '$host AAAA+PTR $val', TTL $ettl"; 4414 } 4415 $ettl = ($ettl < $ttl ? $ettl : $ttl); # use lower TTL 4416 $dbh->do("UPDATE records SET domain_id=?,ttl=?,type=? WHERE record_id=?", undef, 4417 ($domain_id, $ettl, $etype, $erid)); 4418 $nrecs++; 4419 _log($dbh, (group_id => $group, domain_id => $domain_id, rdns_id => $erdns, entry => $logentry) ); 4420 next; # while axfr_next 4421 } 4422 } # $rev eq 'n' 4423 else { 4424 # importing a revzone, we have PTR records that could be merged with matching A/AAAA records 4425 my ($domid,$erid,$ettl,$etype) = $dbh->selectrow_array("SELECT domain_id,record_id,ttl,type FROM records ". 4426 "WHERE host=? AND val=? AND (type=1 OR type=28)", 4427 undef, ($host, $val) ); 4428 if ($erid) { 4429 if ($etype == 1) { # A -> A+PTR 4430 $etype = 65280; 4431 $logentry .= "Merged PTR record with existing matching A record '$host A+PTR $val', TTL $ettl"; 4432 } 4433 if ($etype == 28) { # AAAA -> AAAA+PTR 4434 $etype = 65281; 4435 $logentry .= "Merged PTR record with existing matching AAAA record '$host AAAA+PTR $val', TTL $ettl"; 4436 } 4437 $ettl = ($ettl < $ttl ? $ettl : $ttl); # use lower TTL 4438 $dbh->do("UPDATE records SET rdns_id=?,ttl=?,type=? WHERE record_id=?", undef, 4439 ($rdns_id, $ettl, $etype, $erid)); 4440 $nrecs++; 4441 _log($dbh, (group_id => $group, domain_id => $domid, rdns_id => $rdns_id, entry => $logentry) ); 4442 next; # while axfr_next 4443 } 4444 } # $rev eq 'y' 4445 } # if $merge 4446 4447 # Insert the new record 4448 $sth->execute($domain_id, $rdns_id, $host, $reverse_typemap{$type}, $val, 4449 $distance, $weight, $port, $ttl); 2706 4450 2707 4451 $nrecs++; 2708 4452 4453 if ($type eq 'SOA') { 4454 # also !$rwsoa, but if that's set, it should be impossible to get here. 4455 my @tmp1 = split /:/, $host; 4456 my @tmp2 = split /:/, $val; 4457 $logentry .= "Added SOA record [contact $tmp1[0]] [master $tmp1[1]] ". 4458 "[refresh $tmp2[0]] [retry $tmp2[1]] [expire $tmp2[2]] [minttl $tmp2[3]], TTL $ttl"; 4459 } elsif ($logfrag) { 4460 # special case for log entries we need to meddle with a little. 4461 $logentry .= $logfrag; 4462 } else { 4463 $logentry .= "Added record '$host $type"; 4464 $logentry .= " [distance $distance]" if $type eq 'MX'; 4465 $logentry .= " [priority $distance] [weight $weight] [port $port]" if $type eq 'SRV'; 4466 $logentry .= " $val', TTL $ttl"; 4467 } 4468 _log($dbh, (group_id => $group, domain_id => $domain_id, rdns_id => $rdns_id, entry => $logentry) ); 4469 2709 4470 } # while axfr_next 4471 4472 # Detect and handle delegated subzones 4473 # Placeholder for when we decide what to actually do with this, see previous comments in NS and CNAME handling. 4474 #foreach (keys %suboct) { 4475 # print "found ".($suboct{$_}{ns} ? @{$suboct{$_}{ns}} : '0')." NS records and ". 4476 # ($suboct{$_}{cname} ? @{$suboct{$_}{cname}} : '0')." CNAMEs for $_\n"; 4477 #} 2710 4478 2711 4479 # Overwrite SOA record … … 2716 4484 $sthgetsoa->execute($group,$reverse_typemap{SOA}); 2717 4485 while (my ($host,$val,$ttl) = $sthgetsoa->fetchrow_array()) { 2718 $host =~ s/DOMAIN/$ domain/g;2719 $val =~ s/DOMAIN/$ domain/g;2720 $sthputsoa->execute($ dom_id,$host,$reverse_typemap{SOA},$val,$ttl);4486 $host =~ s/DOMAIN/$zone/g; 4487 $val =~ s/DOMAIN/$zone/g; 4488 $sthputsoa->execute($zone_id,$host,$reverse_typemap{SOA},$val,$ttl); 2721 4489 } 2722 4490 } … … 2729 4497 $sthgetns->execute($group,$reverse_typemap{NS}); 2730 4498 while (my ($host,$val,$ttl) = $sthgetns->fetchrow_array()) { 2731 $host =~ s/DOMAIN/$ domain/g;2732 $val =~ s/DOMAIN/$ domain/g;2733 $sthputns->execute($ dom_id,$host,$reverse_typemap{NS},$val,$ttl);4499 $host =~ s/DOMAIN/$zone/g; 4500 $val =~ s/DOMAIN/$zone/g; 4501 $sthputns->execute($zone_id,$host,$reverse_typemap{NS},$val,$ttl); 2734 4502 } 2735 4503 } … … 2757 4525 2758 4526 4527 ## DNSDB::importBIND() 4528 sub importBIND { 4529 } # end importBIND() 4530 4531 4532 ## DNSDB::import_tinydns() 4533 sub import_tinydns { 4534 } # end import_tinydns() 4535 4536 2759 4537 ## DNSDB::export() 2760 4538 # Export the DNS database, or a part of it … … 2785 4563 2786 4564 ##fixme: slurp up further options to specify particular zone(s) to export 4565 4566 ##fixme: fail if $datafile isn't an open, writable file 4567 4568 # easy case - export all evarything 4569 # not-so-easy case - export item(s) specified 4570 # todo: figure out what kind of list we use to export items 4571 4572 # raw packet in unknown format: first byte indicates length 4573 # of remaining data, allows up to 255 raw bytes 4574 4575 # Locations/views - worth including in the caching setup? 4576 my $lochash = $dbh->selectall_hashref("SELECT location,iplist FROM locations", 'location'); 4577 foreach my $location (keys %$lochash) { 4578 foreach my $ipprefix (split /[,\s]+/, $lochash->{$location}{iplist}) { 4579 $ipprefix =~ s/\s+//g; 4580 print $datafile "%$location:$ipprefix\n"; 4581 } 4582 print $datafile "%$location\n" if !$lochash->{$location}{iplist}; 4583 } 4584 4585 # tracking hash so we don't double-export A+PTR or AAAA+PTR records. 4586 my %recflags; 4587 4588 my $domsth = $dbh->prepare("SELECT domain_id,domain,status,changed FROM domains WHERE status=1"); 4589 my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ". 4590 "FROM records WHERE domain_id=? AND type < 65280"); # Just exclude all types relating to rDNS 4591 my $zonesth = $dbh->prepare("UPDATE domains SET changed='n' WHERE domain_id=?"); 4592 $domsth->execute(); 4593 while (my ($domid,$dom,$domstat,$changed) = $domsth->fetchrow_array) { 4594 ##fixme: need to find a way to block opening symlinked files without introducing a race. 4595 # O_NOFOLLOW 4596 # If pathname is a symbolic link, then the open fails. This is a FreeBSD extension, which was 4597 # added to Linux in version 2.1.126. Symbolic links in earlier components of the pathname will 4598 # still be followed. 4599 # but that doesn't help other platforms. :/ 4600 sysopen(ZONECACHE, "$config{exportcache}/$dom", O_RDWR|O_CREAT); 4601 flock(ZONECACHE, LOCK_EX); 4602 if ($changed || -s "$config{exportcache}/$dom" == 0) { 4603 $recsth->execute($domid); 4604 while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc) = $recsth->fetchrow_array) { 4605 next if $recflags{$recid}; 4606 4607 $loc = '' if !$loc; # de-nullify - just in case 4608 ##fixme: handle case of record-with-location-that-doesn't-exist better. 4609 # note this currently fails safe (tested) - records with a location that 4610 # doesn't exist will not be sent to any client 4611 # $loc = '' if !$lochash->{$loc}; 4612 4613 ##fixme: record validity timestamp. tinydns supports fiddling with timestamps. 4614 # note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps. 4615 # timestamps are TAI64 4616 # ~~ 2^62 + time() 4617 my $stamp = ''; 4618 4619 # support tinydns' auto-TTL 4620 $ttl = '' if $ttl == '0'; 4621 4622 _printrec_tiny($datafile, 'n', \%recflags, 4623 $dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp); 4624 4625 _printrec_tiny(*ZONECACHE, 'n', \%recflags, 4626 $dom, $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp) 4627 if *ZONECACHE; 4628 # in case the zone shrunk, get rid of garbage at the end of the file. 4629 truncate(ZONECACHE, tell(ZONECACHE)); 4630 4631 $recflags{$recid} = 1; 4632 } # while ($recsth) 4633 } else { 4634 # domain not changed, stream from cache 4635 print $datafile $_ while <ZONECACHE>; 4636 } 4637 close ZONECACHE; 4638 # mark domain as unmodified 4639 $zonesth->execute($domid); 4640 } # while ($domsth) 4641 4642 my $revsth = $dbh->prepare("SELECT rdns_id,revnet,status,changed FROM revzones WHERE status=1 ". 4643 "ORDER BY masklen(revnet) DESC"); 4644 4645 # For reasons unknown, we can't sanely UNION these statements. Feh. 4646 # Supposedly it should work though (note last 3 lines): 4647 ## PG manual 4648 #UNION Clause 4649 # 4650 #The UNION clause has this general form: 4651 # 4652 # select_statement UNION [ ALL ] select_statement 4653 # 4654 #select_statement is any SELECT statement without an ORDER BY, LIMIT, FOR UPDATE, or FOR SHARE clause. (ORDER BY 4655 #and LIMIT can be attached to a subexpression if it is enclosed in parentheses. Without parentheses, these 4656 #clauses will be taken to apply to the result of the UNION, not to its right-hand input expression.) 4657 my $soasth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ". 4658 "FROM records WHERE rdns_id=? AND type=6"); 4659 $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl,record_id,location ". 4660 "FROM records WHERE rdns_id=? AND not type=6 ". 4661 "ORDER BY masklen(CAST(val AS inet)) DESC, CAST(val AS inet)"); 4662 $zonesth = $dbh->prepare("UPDATE revzones SET changed='n' WHERE rdns_id=?"); 4663 $revsth->execute(); 4664 while (my ($revid,$revzone,$revstat,$changed) = $revsth->fetchrow_array) { 4665 ##fixme: need to find a way to block opening symlinked files without introducing a race. 4666 # O_NOFOLLOW 4667 # If pathname is a symbolic link, then the open fails. This is a FreeBSD extension, which was 4668 # added to Linux in version 2.1.126. Symbolic links in earlier components of the pathname will 4669 # still be followed. 4670 # but that doesn't help other platforms. :/ 4671 my $tmpzone = NetAddr::IP->new($revzone); 4672 sysopen(ZONECACHE, "$config{exportcache}/".$tmpzone->network->addr, O_RDWR|O_CREAT); 4673 flock(ZONECACHE, LOCK_EX); 4674 if ($changed || -s "$config{exportcache}/".$tmpzone->network->addr == 0) { 4675 # need to fetch this separately since the rest of the records all (should) have real IPs in val 4676 $soasth->execute($revid); 4677 my (@zsoa) = $soasth->fetchrow_array(); 4678 _printrec_tiny($datafile,'y',\%recflags,$revzone, 4679 $zsoa[0],$zsoa[1],$zsoa[2],$zsoa[3],$zsoa[4],$zsoa[5],$zsoa[6],$zsoa[8],''); 4680 4681 $recsth->execute($revid); 4682 while (my ($host,$type,$val,$dist,$weight,$port,$ttl,$recid,$loc) = $recsth->fetchrow_array) { 4683 next if $recflags{$recid}; 4684 4685 $loc = '' if !$loc; # de-nullify - just in case 4686 ##fixme: handle case of record-with-location-that-doesn't-exist better. 4687 # note this currently fails safe (tested) - records with a location that 4688 # doesn't exist will not be sent to any client 4689 # $loc = '' if !$lochash->{$loc}; 4690 4691 ##fixme: record validity timestamp. tinydns supports fiddling with timestamps. 4692 # note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps. 4693 # timestamps are TAI64 4694 # ~~ 2^62 + time() 4695 my $stamp = ''; 4696 4697 # support tinydns' auto-TTL 4698 $ttl = '' if $ttl == '0'; 4699 4700 _printrec_tiny($datafile, 'y', \%recflags, $revzone, 4701 $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp); 4702 _printrec_tiny(*ZONECACHE, 'y', \%recflags, $revzone, 4703 $host, $type, $val, $dist, $weight, $port, $ttl, $loc, $stamp) 4704 if *ZONECACHE; 4705 # in case the zone shrunk, get rid of garbage at the end of the file. 4706 truncate(ZONECACHE, tell(ZONECACHE)); 4707 4708 $recflags{$recid} = 1; 4709 } # while ($recsth) 4710 } else { 4711 # zone not changed, stream from cache 4712 print $datafile $_ while <ZONECACHE>; 4713 } 4714 close ZONECACHE; 4715 # mark domain as unmodified 4716 $zonesth->execute($revid); 4717 } # while ($domsth) 4718 4719 } # end __export_tiny() 4720 4721 4722 # Utility sub for __export_tiny above 4723 sub _printrec_tiny { 4724 my ($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp) = @_; 2787 4725 2788 4726 ## Convert a bare number into an octal-coded pair of octets. … … 2797 4735 } 2798 4736 2799 ##fixme: fail if $datafile isn't an open, writable file 2800 2801 # easy case - export all evarything 2802 # not-so-easy case - export item(s) specified 2803 # todo: figure out what kind of list we use to export items 2804 2805 my $domsth = $dbh->prepare("SELECT domain_id,domain,status FROM domains WHERE status=1"); 2806 my $recsth = $dbh->prepare("SELECT host,type,val,distance,weight,port,ttl ". 2807 "FROM records WHERE domain_id=?"); 2808 $domsth->execute(); 2809 while (my ($domid,$dom,$domstat) = $domsth->fetchrow_array) { 2810 $recsth->execute($domid); 2811 while (my ($host,$type,$val,$dist,$weight,$port,$ttl) = $recsth->fetchrow_array) { 2812 ##fixme: need to store location in the db, and retrieve it here. 2813 # temporarily hardcoded to empty so we can include it further down. 2814 my $loc = ''; 2815 2816 ##fixme: record validity timestamp. tinydns supports fiddling with timestamps. 2817 # note $ttl must be set to 0 if we want to use tinydns's auto-expiring timestamps. 2818 # timestamps are TAI64 2819 # ~~ 2^62 + time() 2820 my $stamp = ''; 2821 2822 # raw packet in unknown format: first byte indicates length 2823 # of remaining data, allows up to 255 raw bytes 2824 2825 # Spaces are evil. 2826 $host =~ s/^\s+//; 2827 $host =~ s/\s+$//; 2828 if ($typemap{$type} ne 'TXT') { 2829 # Leading or trailng spaces could be legit in TXT records. 2830 $val =~ s/^\s+//; 2831 $val =~ s/\s+$//; 2832 } 4737 ## WARNING: This works to export even the whole Internet's worth of IP space... 4738 ## if you have the disk/RAM to handle the dataset, and you call this sub based on /16-sized chunks 4739 ## A /16 took ~3 seconds with a handful of separate records; adding a /8 pushed export time out to ~13m:40s 4740 ## 0/0 is estimated to take ~54 hours and ~256G of disk 4741 ## RAM usage depends on how many non-template entries you have in the set. 4742 ## This should probably be done on record addition rather than export; large blocks may need to be done in a 4743 ## forked process 4744 sub __publish_subnet { 4745 my $sub = shift; 4746 my $recflags = shift; 4747 my $hpat = shift; 4748 my $fh = shift; 4749 my $ttl = shift; 4750 my $stamp = shift; 4751 my $loc = shift; 4752 my $ptronly = shift || 0; 4753 4754 my $iplist = $sub->splitref(32); 4755 foreach (@$iplist) { 4756 my $ip = $_->addr; 4757 # make as if we split the non-octet-aligned block into octet-aligned blocks as with SOA 4758 next if $ip =~ /\.(0|255)$/; 4759 next if $$recflags{$ip}; 4760 $$recflags{$ip}++; 4761 my $rec = $hpat; # start fresh with the template for each IP 4762 _template4_expand(\$rec, $ip); 4763 print $fh ($ptronly ? "^"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$rec" : "=$rec:$ip"). 4764 ":$ttl:$stamp:$loc\n"; 4765 } 4766 } 2833 4767 2834 4768 ##fixme? append . to all host/val hostnames … … 2841 4775 my ($email, $primary) = (split /:/, $host)[0,1]; 2842 4776 my ($refresh, $retry, $expire, $min_ttl) = (split /:/, $val)[0,1,2,3]; 2843 print $datafile "Z$dom:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"; 4777 if ($revrec eq 'y') { 4778 ##fixme: have to publish SOA records for each v4 /24 in sub-/16, and each /16 in sub-/8 4779 # what about v6? 4780 # -> only need SOA for local chunks offset from reverse delegation boundaries, so v6 is fine 4781 $zone = NetAddr::IP->new($zone); 4782 # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones 4783 if (!$zone->{isv6} && ($zone->masklen < 24) && ($zone->masklen % 8 != 0)) { 4784 foreach my $szone ($zone->split($zone->masklen + (8 - $zone->masklen % 8))) { 4785 $szone = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.'); 4786 print $datafile "Z$szone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"; 4787 } 4788 return; # skips "default" bits just below 4789 } 4790 $zone = _ZONE($zone, 'ZONE', 'r', '.').($zone->{isv6} ? '.ip6.arpa' : '.in-addr.arpa'); 4791 } 4792 print $datafile "Z$zone:$primary:$email"."::$refresh:$retry:$expire:$min_ttl:$ttl:$stamp:$loc\n"; 2844 4793 2845 4794 } elsif ($typemap{$type} eq 'A') { … … 2849 4798 } elsif ($typemap{$type} eq 'NS') { 2850 4799 2851 print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n"; 4800 if ($revrec eq 'y') { 4801 $val = NetAddr::IP->new($val); 4802 # handle split-n-multiply SOA for off-octet (8 < mask < 16) or (16 < mask < 24) v4 zones 4803 if (!$val->{isv6} && ($val->masklen < 24) && ($val->masklen % 8 != 0)) { 4804 foreach my $szone ($val->split($val->masklen + (8 - $val->masklen % 8))) { 4805 my $szone2 = _ZONE($szone, 'ZONE.in-addr.arpa', 'r', '.'); 4806 next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen; 4807 print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n"; 4808 $$recflags{$szone2} = $val->masklen; 4809 } 4810 } elsif ($val->{isv6} && ($val->masklen < 64) && ($val->masklen % 4 !=0)) { 4811 foreach my $szone ($val->split($val->masklen + (4 - $val->masklen % 4))) { 4812 my $szone2 = _ZONE($szone, 'ZONE.ip6.arpa', 'r', '.'); 4813 next if $$recflags{$szone2} && $$recflags{$szone2} > $val->masklen; 4814 print $datafile "\&$szone2"."::$host:$ttl:$stamp:$loc\n"; 4815 $$recflags{$szone2} = $val->masklen; 4816 } 4817 } else { 4818 my $val2 = _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa'); 4819 print $datafile "\&$val2"."::$host:$ttl:$stamp:$loc\n"; 4820 $$recflags{$val2} = $val->masklen; 4821 } 4822 } else { 4823 print $datafile "\&$host"."::$val:$ttl:$stamp:$loc\n"; 4824 } 2852 4825 2853 4826 } elsif ($typemap{$type} eq 'AAAA') { … … 2881 4854 2882 4855 ##fixme: split v-e-r-y long TXT strings? will need to do so for BIND export, at least 2883 $val =~ s/:/\\072/g; # may need to replace other symbols 2884 print $datafile "'$host:$val:$ttl:$stamp:$loc\n"; 4856 if ($revrec eq 'n') { 4857 $val =~ s/:/\\072/g; # may need to replace other symbols 4858 print $datafile "'$host:$val:$ttl:$stamp:$loc\n"; 4859 } else { 4860 $host =~ s/:/\\072/g; # may need to replace other symbols 4861 my $val2 = NetAddr::IP->new($val); 4862 print $datafile "'"._ZONE($val2, 'ZONE', 'r', '.').($val2->{isv6} ? '.ip6.arpa' : '.in-addr.arpa'). 4863 ":$host:$ttl:$stamp:$loc\n"; 4864 } 2885 4865 2886 4866 # by-hand TXT … … 2903 4883 } elsif ($typemap{$type} eq 'CNAME') { 2904 4884 2905 print $datafile "C$host:$val:$ttl:$stamp:$loc\n"; 4885 if ($revrec eq 'n') { 4886 print $datafile "C$host:$val:$ttl:$stamp:$loc\n"; 4887 } else { 4888 my $val2 = NetAddr::IP->new($val); 4889 print $datafile "C"._ZONE($val2, 'ZONE', 'r', '.').($val2->{isv6} ? '.ip6.arpa' : '.in-addr.arpa'). 4890 ":$host:$ttl:$stamp:$loc\n"; 4891 } 2906 4892 2907 4893 } elsif ($typemap{$type} eq 'SRV') { … … 2936 4922 } elsif ($typemap{$type} eq 'PTR') { 2937 4923 2938 # must handle both IPv4 and IPv6 2939 ##work 2940 # data should already be in suitable reverse order. 2941 print $datafile "^$host:$val:$ttl:$stamp:$loc\n"; 4924 $zone = NetAddr::IP->new($zone); 4925 $$recflags{$val}++; 4926 if (!$zone->{isv6} && $zone->masklen > 24) { 4927 ($val) = ($val =~ /\.(\d+)$/); 4928 print $datafile "^$val."._ZONE($zone, 'ZONE', 'r', '.').'.in-addr.arpa'. 4929 ":$host:ttl:$stamp:$loc\n"; 4930 } else { 4931 $val = NetAddr::IP->new($val); 4932 print $datafile "^". 4933 _ZONE($val, 'ZONE', 'r', '.').($val->{isv6} ? '.ip6.arpa' : '.in-addr.arpa'). 4934 ":$host:$ttl:$stamp:$loc\n"; 4935 } 4936 4937 } elsif ($type == 65280) { # A+PTR 4938 4939 $$recflags{$val}++; 4940 print $datafile "=$host:$val:$ttl:$stamp:$loc\n"; 4941 4942 } elsif ($type == 65281) { # AAAA+PTR 4943 4944 #$$recflags{$val}++; 4945 # treat these as two separate records. since tinydns doesn't have 4946 # a native combined type, we have to create them separately anyway. 4947 if ($revrec eq 'n') { 4948 $type = 28; 4949 } else { 4950 $type = 12; 4951 } 4952 _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,$type,$val,$dist,$weight,$port,$ttl,$loc,$stamp); 4953 ##fixme: add a config flag to indicate use of the patch from http://www.fefe.de/dns/ 4954 # type 6 is for AAAA+PTR, type 3 is for AAAA 4955 4956 } elsif ($type == 65282) { # PTR template 4957 4958 # only useful for v4 with standard DNS software, since this expands all 4959 # IPs in $zone (or possibly $val?) with autogenerated records 4960 $val = NetAddr::IP->new($val); 4961 return if $val->{isv6}; 4962 4963 if ($val->masklen <= 16) { 4964 foreach my $sub ($val->split(16)) { 4965 __publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, 1); 4966 } 4967 } else { 4968 __publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, 1); 4969 } 4970 4971 } elsif ($type == 65283) { # A+PTR template 4972 4973 $val = NetAddr::IP->new($val); 4974 # Just In Case. An A+PTR should be impossible to add to a v6 revzone via API. 4975 return if $val->{isv6}; 4976 4977 if ($val->masklen <= 16) { 4978 foreach my $sub ($val->split(16)) { 4979 __publish_subnet($sub, $recflags, $host, $datafile, $ttl, $stamp, $loc, 0); 4980 } 4981 } else { 4982 __publish_subnet($val, $recflags, $host, $datafile, $ttl, $stamp, $loc, 0); 4983 } 4984 4985 } elsif ($type == 65284) { # AAAA+PTR template 4986 # Stub for completeness. Could be exported to DNS software that supports 4987 # some degree of internal automagic in generic-record-creation 4988 # (eg http://search.cpan.org/dist/AllKnowingDNS/ ) 4989 4990 } elsif ($type == 65285) { # Delegation 4991 # This is intended for reverse zones, but may prove useful in forward zones. 4992 4993 # All delegations need to create one or more NS records. The NS record handler knows what to do. 4994 _printrec_tiny($datafile,$revrec,$recflags,$zone,$host,$reverse_typemap{'NS'}, 4995 $val,$dist,$weight,$port,$ttl,$loc,$stamp); 4996 if ($revrec eq 'y') { 4997 # In the case of a sub-/24 v4 reverse delegation, we need to generate CNAMEs 4998 # to redirect all of the individual IP lookups as well. 4999 # Not sure how this would actually resolve if a /24 or larger was delegated 5000 # one way, and a sub-/24 in that >=/24 was delegated elsewhere... 5001 my $dblock = NetAddr::IP->new($val); 5002 if (!$dblock->{isv6} && $dblock->masklen > 24) { 5003 my @subs = $dblock->split; 5004 foreach (@subs) { 5005 next if $$recflags{"$_"}; 5006 my ($oct) = ($_->addr =~ /(\d+)$/); 5007 print $datafile "C"._ZONE($_, 'ZONE.in-addr.arpa', 'r', '.').":$oct.". 5008 _ZONE($dblock, 'ZONE.in-addr.arpa', 'r', '.').":$ttl:$stamp:$loc\n"; 5009 $$recflags{"$_"}++; 5010 } 5011 } 5012 } 5013 5014 ## 5015 ## Uncommon types. These will need better UI support Any Day Sometime Maybe(TM). 5016 ## 5017 5018 } elsif ($type == 44) { # SSHFP 5019 my ($algo,$fpt,$fp) = split /\s+/, $val; 5020 5021 my $rec = sprintf ":$host:44:\\%0.3o\\%0.3o", $algo, $fpt; 5022 while (my ($byte) = ($fp =~ /^(..)/) ) { 5023 $rec .= sprintf "\\%0.3o", hex($byte); 5024 $fp =~ s/^..//; 5025 } 5026 print $datafile "$rec:$ttl:$stamp:$loc\n"; 2942 5027 2943 5028 } else { … … 2954 5039 } # record type if-else 2955 5040 2956 } # while ($recsth) 2957 } # while ($domsth) 2958 } # end __export_tiny() 5041 } # end _printrec_tiny() 2959 5042 2960 5043 2961 5044 ## DNSDB::mailNotify() 2962 # Sends notification mail to recipients regarding a n IPDB operation5045 # Sends notification mail to recipients regarding a DNSDB operation 2963 5046 sub mailNotify { 2964 5047 my $dbh = shift; -
branches/stable/dns-1.0-1.2.sql
r365 r545 1 1 -- SQL table/record type upgrade file for dnsadmin 1.0 to 1.2 migration 2 3 -- need this before we add any other bits 4 CREATE TABLE locations ( 5 location character varying (4) PRIMARY KEY, 6 loc_id serial UNIQUE, 7 group_id integer NOT NULL DEFAULT 1, 8 iplist text NOT NULL DEFAULT '', 9 description character varying(40) NOT NULL DEFAULT '', 10 comments text NOT NULL DEFAULT '' 11 ); 12 13 ALTER TABLE ONLY locations 14 ADD CONSTRAINT "locations_group_id_fkey" FOREIGN KEY (group_id) REFERENCES groups(group_id); 15 16 ALTER TABLE permissions ADD COLUMN record_locchg boolean DEFAULT false NOT NULL; 17 ALTER TABLE permissions ADD COLUMN location_create boolean DEFAULT false NOT NULL; 18 ALTER TABLE permissions ADD COLUMN location_edit boolean DEFAULT false NOT NULL; 19 ALTER TABLE permissions ADD COLUMN location_delete boolean DEFAULT false NOT NULL; 20 ALTER TABLE permissions ADD COLUMN location_view boolean DEFAULT false NOT NULL; 2 21 3 22 -- Minor buglet; domains must be unique … … 23 42 SELECT pg_catalog.setval('default_rev_records_record_id_seq', 5, false); 24 43 44 ALTER TABLE domains ADD COLUMN changed boolean DEFAULT true NOT NULL; 45 ALTER TABLE domains ADD COLUMN default_location character varying (4) DEFAULT '' NOT NULL; 46 -- ~2x performance boost iff most zones are fed to output from the cache 47 CREATE INDEX dom_status_index ON domains (status); 48 25 49 CREATE TABLE revzones ( 26 50 rdns_id serial NOT NULL, … … 30 54 status integer DEFAULT 1 NOT NULL, 31 55 zserial integer, 32 sertype character(1) DEFAULT 'D'::bpchar 56 sertype character(1) DEFAULT 'D'::bpchar, 57 changed boolean DEFAULT true NOT NULL, 58 default_location character varying (4) DEFAULT '' NOT NULL 33 59 ); 60 CREATE INDEX rev_status_index ON revzones (status); 61 62 ALTER TABLE ONLY revzones 63 ADD CONSTRAINT "$1" FOREIGN KEY (group_id) REFERENCES groups(group_id); 34 64 35 65 ALTER TABLE log ADD COLUMN rdns_id INTEGER; … … 40 70 ALTER TABLE records DROP CONSTRAINT "$1"; 41 71 ALTER TABLE records ALTER COLUMN domain_id SET DEFAULT 0; 42 ALTER TABLE records ADD COLUMN rdns_id INTEGER DEFAULT 0; 43 UPDATE records SET rdns_id=0; 44 ALTER TABLE records ALTER COLUMN rdns_id SET NOT NULL; 72 ALTER TABLE records ADD COLUMN rdns_id INTEGER DEFAULT 0 NOT NULL; 73 ALTER TABLE records ADD COLUMN location character varying (4) DEFAULT '' NOT NULL; 74 75 -- ~120s -> 75s performance boost on 100K records when always exporting all records 76 CREATE INDEX rec_types_index ON records (type); 77 -- Further ~1/3 performance gain, same dataset 78 CREATE INDEX rec_domain_index ON records (domain_id); 79 CREATE INDEX rec_revzone_index ON records (rdns_id); 45 80 46 81 -- May as well drop and recreate; this is nominally static and loaded from the -
branches/stable/dns-rpc.cgi
r263 r545 3 3 ## 4 4 # $Id$ 5 # Copyright 201 1Kris Deugau <kdeugau@deepnet.cx>5 # Copyright 2012 Kris Deugau <kdeugau@deepnet.cx> 6 6 # 7 7 # This program is free software: you can redistribute it and/or modify … … 39 39 #package main; 40 40 41 loadConfig();41 DNSDB::loadConfig(rpcflag => 1); 42 42 43 43 # need to create a DNSDB object too … … 49 49 my $methods = { 50 50 'dnsdb.addDomain' => \&addDomain, 51 'dnsdb.delDomain' => \&delDomain, 51 'dnsdb.delZone' => \&delZone, 52 'dnsdb.addRDNS' => \&addRDNS, 52 53 'dnsdb.addGroup' => \&addGroup, 53 54 'dnsdb.delGroup' => \&delGroup, … … 60 61 'dnsdb.getRecCount' => \&getRecCount, 61 62 'dnsdb.addRec' => \&addRec, 63 'dnsdb.updateRec' => \&updateRec, 62 64 'dnsdb.delRec' => \&delRec, 63 'dnsdb. domStatus' => \&domStatus,65 'dnsdb.zoneStatus' => \&zoneStatus, 64 66 65 67 'dnsdb.getMethods' => \&get_method_list … … 71 73 72 74 # "Can't do that" errors 73 ##fixme: this MUST be loaded from a config file! Also must support multiple IPs74 if ($ENV{REMOTE_ADDR} ne '192.168.2.116') {75 print "Content-type: text/xml\n\n".$res->{_decode}->encode_fault(5, "Access denied");76 exit;77 }78 75 if (!$dbh) { 79 76 print "Content-type: text/xml\n\n".$res->{_decode}->encode_fault(5, $msg); … … 89 86 ## Subs below here 90 87 ## 88 89 # Utility subs 90 sub _aclcheck { 91 my $subsys = shift; 92 return 1 if grep /$ENV{REMOTE_ADDR}/, @{$DNSDB::config{rpcacl}{$subsys}}; 93 return 0; 94 } 95 96 # Let's see if we can factor these out of the RPC method subs 97 sub _commoncheck { 98 my $argref = shift; 99 my $needslog = shift; 100 101 die "Missing remote system name\n" if !$argref->{rpcsystem}; 102 die "Access denied\n" if !_aclcheck($argref->{rpcsystem}); 103 if ($needslog) { 104 die "Missing remote username\n" if !$argref->{rpcuser}; 105 die "Couldn't set userdata for logging\n" 106 unless DNSDB::initRPC($dbh, (username => $argref->{rpcuser}, rpcsys => $argref->{rpcsystem}, 107 fullname => ($argref->{fullname} ? $argref->{fullname} : $argref->{rpcuser}) ) ); 108 } 109 } 91 110 92 111 #sub connectDB { … … 103 122 my %args = @_; 104 123 105 # Make sure we've got all the local bits we need 106 die "Missing remote username" if !$args{rpcuser}; # for logging 107 die "Missing remote system name" if !$args{rpcsystem}; # for logging 124 _commoncheck(\%args, 'y'); 108 125 109 126 my ($code, $msg) = DNSDB::addDomain($dbh, $args{domain}, $args{group}, $args{state}); … … 112 129 } 113 130 114 sub delDomain { 115 my %args = @_; 116 117 # Make sure we've got all the local bits we need 118 die "Missing remote username" if !$args{rpcuser}; # for logging 119 die "Missing remote system name" if !$args{rpcsystem}; # for logging 131 sub delZone { 132 my %args = @_; 133 134 _commoncheck(\%args, 'y'); 135 die "Need forward/reverse zone flag\n" if !$args{revrec}; 120 136 121 137 my ($code,$msg); 122 # Let's be nice; delete based on domid OR domainname. Saves an RPC call round-trip, maybe.123 if ($args{ domain} =~ /^\d+$/) {124 ($code,$msg) = DNSDB::del Domain($dbh, $args{domain});138 # Let's be nice; delete based on zone id OR zone name. Saves an RPC call round-trip, maybe. 139 if ($args{zone} =~ /^\d+$/) { 140 ($code,$msg) = DNSDB::delZone($dbh, $args{zone}, $args{revrec}); 125 141 } else { 126 my $domid = DNSDB::domainID($dbh, $args{domain}); 127 die "Can't find domain" if !$domid; 128 ($code,$msg) = DNSDB::delDomain($dbh, $domid); 129 } 130 die $msg if $code eq 'FAIL'; 131 } 132 133 #sub domainName { 134 #sub domainID { 142 my $zoneid; 143 $zoneid = DNSDB::domainID($dbh, $args{zone}) if $args{revrec} eq 'n'; 144 $zoneid = DNSDB::revID($dbh, $args{zone}) if $args{revrec} eq 'y'; 145 die "Can't find zone: $DNSDB::errstr\n" if !$zoneid; 146 ($code,$msg) = DNSDB::delZone($dbh, $zoneid, $args{revrec}); 147 } 148 die $msg if $code eq 'FAIL'; 149 return $msg; 150 } 151 152 #sub domainName {} 153 #sub revName {} 154 #sub domainID {} 155 #sub revID {} 156 157 sub addRDNS { 158 my %args = @_; 159 160 _commoncheck(\%args, 'y'); 161 162 my ($code, $msg) = DNSDB::addRDNS($dbh, $args{revzone}, $args{revpatt}, $args{group}, $args{state}); 163 die $msg if $code eq 'FAIL'; 164 return $msg; # domain ID 165 } 166 167 #sub getZoneCount {} 168 #sub getZoneList {} 169 #sub getZoneLocation {} 135 170 136 171 sub addGroup { 137 172 my %args = @_; 138 173 139 # Make sure we've got all the local bits we need140 die "Missing remote username" if !$args{rpcuser}; # for logging141 die "Missing remote system name" if !$args{rpcsystem}; # for logging142 143 # not sure how to usefully represent permissions from any further out from DNSDB.pm:/174 _commoncheck(\%args, 'y'); 175 die "Missing new group name\n" if !$args{groupname}; 176 die "Missing parent group ID\n" if !$args{parent_id}; 177 178 # not sure how to usefully represent permissions via RPC. :/ 144 179 # not to mention, permissions are checked at the UI layer, not the DB layer. 145 180 my $perms = {domain_edit => 1, domain_create => 1, domain_delete => 1, … … 155 190 my %args = @_; 156 191 157 # Make sure we've got all the local bits we need 158 die "Missing remote username" if !$args{rpcuser}; # for logging 159 die "Missing remote system name" if !$args{rpcsystem}; # for logging 192 _commoncheck(\%args, 'y'); 193 die "Missing group ID or name to remove\n" if !$args{group}; 160 194 161 195 my ($code,$msg); … … 165 199 } else { 166 200 my $grpid = DNSDB::groupID($dbh, $args{group}); 167 die "Can't find group " if !$grpid;201 die "Can't find group\n" if !$grpid; 168 202 ($code,$msg) = DNSDB::delGroup($dbh, $grpid); 169 203 } 170 204 die $msg if $code eq 'FAIL'; 171 } 172 173 #sub getChildren { 174 #sub groupName { 175 #sub groupID { 205 return $msg; 206 } 207 208 #sub getChildren {} 209 #sub groupName {} 210 #sub getGroupCount {} 211 #sub getGroupList {} 212 #sub groupID {} 176 213 177 214 sub addUser { 178 215 my %args = @_; 179 216 180 # Make sure we've got all the local bits we need 181 die "Missing remote username" if !$args{rpcuser}; # for logging 182 die "Missing remote system name" if !$args{rpcsystem}; # for logging 183 184 # not sure how to usefully represent permissions from any further out from DNSDB.pm :/ 217 _commoncheck(\%args, 'y'); 218 219 # not sure how to usefully represent permissions via RPC. :/ 185 220 # not to mention, permissions are checked at the UI layer, not the DB layer. 186 my $perms = {domain_edit => 1, domain_create => 1, domain_delete => 1,187 record_edit => 1, record_create => 1, record_delete => 1188 };189 221 # bend and twist; get those arguments in in the right order! 190 222 $args{type} = 'u' if !$args{type}; … … 200 232 } 201 233 202 #sub checkUser { 234 #sub getUserCount {} 235 #sub getUserList {} 236 #sub getUserDropdown {} 237 #sub checkUser {} 203 238 204 239 sub updateUser { 205 240 my %args = @_; 206 241 207 # Make sure we've got all the local bits we need 208 die "Missing remote username" if !$args{rpcuser}; # for logging 209 die "Missing remote system name" if !$args{rpcsystem}; # for logging 210 211 die "Missing UID" if !$args{uid}; 212 213 # not sure how to usefully represent permissions from any further out from DNSDB.pm :/ 214 # not to mention, permissions are checked at the UI layer, not the DB layer. 215 my $perms = {domain_edit => 1, domain_create => 1, domain_delete => 1, 216 record_edit => 1, record_create => 1, record_delete => 1 217 }; 242 _commoncheck(\%args, 'y'); 243 244 die "Missing UID\n" if !$args{uid}; 245 218 246 # bend and twist; get those arguments in in the right order! 247 $args{type} = 'u' if !$args{type}; 219 248 my @userargs = ($args{uid}, $args{username}, $args{group}, $args{pass}, $args{state}, $args{type}); 220 249 for my $argname ('fname','lname','phone') { … … 224 253 ##fixme: also underlying in DNSDB::updateUser(): no way to just update this or that attribute; 225 254 # have to pass them all in to be overwritten 226 my ($code,$msg) = DNSDB::addUser($dbh, @userargs); 227 die $msg if $code eq 'FAIL'; 255 my ($code,$msg) = DNSDB::updateUser($dbh, @userargs); 256 die $msg if $code eq 'FAIL'; 257 return $msg; 228 258 } 229 259 … … 231 261 my %args = @_; 232 262 233 # Make sure we've got all the local bits we need 234 die "Missing remote username" if !$args{rpcuser}; # for logging 235 die "Missing remote system name" if !$args{rpcsystem}; # for logging 236 237 die "Missing UID" if !$args{uid}; 263 _commoncheck(\%args, 'y'); 264 265 die "Missing UID\n" if !$args{uid}; 238 266 my ($code,$msg) = DNSDB::delUser($dbh, $args{uid}); 239 267 die $msg if $code eq 'FAIL'; 240 } 241 242 #sub userFullName { 243 #sub userStatus { 244 #sub getUserData { 268 return $msg; 269 } 270 271 #sub userFullName {} 272 #sub userStatus {} 273 #sub getUserData {} 274 275 #sub addLoc {} 276 #sub updateLoc {} 277 #sub delLoc {} 278 #sub getLoc {} 279 #sub getLocCount {} 280 #sub getLocList {} 281 #sub getLocDropdown {} 245 282 246 283 sub getSOA { 247 284 my %args = @_; 248 285 249 # Make sure we've got all the local bits we need 250 die "Missing remote username" if !$args{rpcuser}; # for logging 251 die "Missing remote system name" if !$args{rpcsystem}; # for logging 252 253 my %ret = DNSDB::getSOA($dbh, $args{def}, $args{id}); 254 if (!$ret{recid}) { 255 if ($args{def} eq 'y') { 256 die "No default SOA record in group"; 286 _commoncheck(\%args); 287 288 my $ret = DNSDB::getSOA($dbh, $args{defrec}, $args{revrec}, $args{id}); 289 if (!$ret) { 290 if ($args{defrec} eq 'y') { 291 die "No default SOA record in group\n"; 257 292 } else { 258 die "No SOA record in domain";293 die "No SOA record in zone\n"; 259 294 } 260 295 } 261 return \%ret; 262 } 296 return $ret; 297 } 298 299 #sub updateSOA {} 263 300 264 301 sub getRecLine { 265 302 my %args = @_; 266 303 267 # Make sure we've got all the local bits we need 268 die "Missing remote username" if !$args{rpcuser}; # for logging 269 die "Missing remote system name" if !$args{rpcsystem}; # for logging 270 271 my $ret = DNSDB::getRecLine($dbh, $args{def}, $args{id}); 304 _commoncheck(\%args); 305 306 my $ret = DNSDB::getRecLine($dbh, $args{defrec}, $args{revrec}, $args{id}); 272 307 273 308 die $DNSDB::errstr if !$ret; … … 279 314 my %args = @_; 280 315 281 # Make sure we've got all the local bits we need 282 die "Missing remote username" if !$args{rpcuser}; # for logging 283 die "Missing remote system name" if !$args{rpcsystem}; # for logging 284 285 #bleh 316 _commoncheck(\%args); 317 318 # set some optional args 286 319 $args{nrecs} = 'all' if !$args{nrecs}; 287 320 $args{nstart} = 0 if !$args{nstart}; … … 290 323 $args{direction} = 'ASC' if !$args{direction}; 291 324 292 my $ret = DNSDB::getDomRecs($dbh, $args{def}, $args{id}, $args{nrecs}, $args{nstart}, $args{order}, $args{direction}); 325 my $ret = DNSDB::getDomRecs($dbh, (defrec => $args{defrec}, revrec => $args{revrec}, id => $args{id}, 326 offset => $args{offset}, sortby => $args{sortby}, sortorder => $args{sortorder}, 327 filter => $args{filter}) ); 293 328 294 329 die $DNSDB::errstr if !$ret; … … 300 335 my %args = @_; 301 336 302 # Make sure we've got all the local bits we need 303 die "Missing remote username" if !$args{rpcuser}; # for logging 304 die "Missing remote system name" if !$args{rpcsystem}; # for logging 305 306 return DNSDB::getRecCount($dbh, $id); 337 _commoncheck(\%args); 338 339 # set some optional args 340 $args{nrecs} = 'all' if !$args{nrecs}; 341 $args{nstart} = 0 if !$args{nstart}; 342 ## for order, need to map input to column names 343 $args{order} = 'host' if !$args{order}; 344 $args{direction} = 'ASC' if !$args{direction}; 345 346 my $ret = DNSDB::getRecCount($dbh, $args{defrec}, $args{revrec}, $args{id}, $args{filter}); 347 348 die $DNSDB::errstr if !$ret; 349 350 return $ret; 307 351 } 308 352 … … 310 354 my %args = @_; 311 355 312 # Make sure we've got all the local bits we need 313 die "Missing remote username" if !$args{rpcuser}; # for logging 314 die "Missing remote system name" if !$args{rpcsystem}; # for logging 315 316 # note dist, weight, port are not reequired on all types; will be ignored if not needed. 317 my ($code, $msg) = DNSDB::addRec($dbh, $args{def}, $args{domid}, $args{host}, $typemap{$args{type}}, 356 _commoncheck(\%args, 'y'); 357 358 # note dist, weight, port are not required on all types; will be ignored if not needed. 359 my ($code, $msg) = DNSDB::addRec($dbh, $args{def}, $args{domid}, $args{host}, $DNSDB::typemap{$args{type}}, 318 360 $args{val}, $args{ttl}, $args{dist}, $args{weight}, $args{port}); 319 361 … … 324 366 my %args = @_; 325 367 326 # Make sure we've got all the local bits we need 327 die "Missing remote username" if !$args{rpcuser}; # for logging 328 die "Missing remote system name" if !$args{rpcsystem}; # for logging 329 330 # note dist, weight, port are not reequired on all types; will be ignored if not needed. 331 my ($code, $msg) = DNSDB::updateRec($dbh, $args{def}, $args{recid}, $args{host}, $typemap{$args{type}}, 368 _commoncheck(\%args, 'y'); 369 370 # note dist, weight, port are not required on all types; will be ignored if not needed. 371 my ($code, $msg) = DNSDB::updateRec($dbh, $args{def}, $args{recid}, $args{host}, $DNSDB::typemap{$args{type}}, 332 372 $args{val}, $args{ttl}, $args{dist}, $args{weight}, $args{port}); 333 373 … … 338 378 my %args = @_; 339 379 340 # Make sure we've got all the local bits we need 341 die "Missing remote username" if !$args{rpcuser}; # for logging 342 die "Missing remote system name" if !$args{rpcsystem}; # for logging 343 344 # note dist, weight, port are not reequired on all types; will be ignored if not needed. 380 _commoncheck(\%args, 'y'); 381 345 382 my ($code, $msg) = DNSDB::delRec($dbh, $args{def}, $args{recid}); 346 383 … … 348 385 } 349 386 350 #sub getParents { 351 352 sub domStatus { 353 my %args = @_; 354 355 # Make sure we've got all the local bits we need 356 die "Missing remote username" if !$args{rpcuser}; # for logging 357 die "Missing remote system name" if !$args{rpcsystem}; # for logging 358 359 my @arglist = ($dbh, $args{domid}); 387 #sub getLogCount {} 388 #sub getLogEntries {} 389 #sub getTypelist {} 390 #sub parentID {} 391 #sub isParent {} 392 393 sub zoneStatus { 394 my %args = @_; 395 396 _commoncheck(\%args, 'y'); 397 398 my @arglist = ($dbh, $args{zoneid}); 360 399 push @arglist, $args{status} if defined($args{status}); 361 400 362 my $status = DNSDB::domStatus(@arglist); 363 } 364 365 #sub importAXFR { 366 #sub export { 367 #sub __export_tiny { 401 my $status = DNSDB::zoneStatus(@arglist); 402 } 403 404 #sub importAXFR {} 405 #sub importBIND {} 406 #sub import_tinydns {} 407 #sub export {} 408 #sub __export_tiny {} 409 #sub _printrec_tiny {} 410 #sub mailNotify {} 368 411 369 412 sub get_method_list { -
branches/stable/dns.cgi
r544 r545 3 3 ## 4 4 # $Id$ 5 # Copyright 2008-201 1Kris Deugau <kdeugau@deepnet.cx>5 # Copyright 2008-2012 Kris Deugau <kdeugau@deepnet.cx> 6 6 # 7 7 # This program is free software: you can redistribute it and/or modify … … 96 96 $session->param('reclistsortby','host'); 97 97 $session->param('reclistorder','ASC'); 98 $session->param('loclistsortby','description'); 99 $session->param('loclistorder','ASC'); 100 $session->param('logsortby','stamp'); 101 $session->param('logorder','DESC'); 98 102 } 99 103 … … 122 126 $webvar{startwith} =~ s/^(0-9|[a-z]).*/$1/ if $webvar{startwith}; 123 127 # not much call for chars not allowed in domain names 124 $webvar{filter} =~ s/[^a-zA-Z0-9_.: @-]//g if $webvar{filter};128 $webvar{filter} =~ s/[^a-zA-Z0-9_.:\@-]//g if $webvar{filter}; 125 129 ## only set 'y' if box is checked, no other values legal 126 130 ## however, see https://secure.deepnet.cx/trac/dnsadmin/ticket/31 … … 227 231 if ($webvar{action} eq 'login') { 228 232 # Snag ACL/permissions here too 229 my $sth = $dbh->prepare("SELECT user_id,group_id,password,firstname,lastname,status FROM users WHERE username=?"); 230 $sth->execute($webvar{username}); 231 232 if (my ($uid,$gid,$pass,$fname,$lname,$status) = $sth->fetchrow_array) { 233 $webvar{password} = '' if !$webvar{password}; 234 if (!$status) { 235 $webvar{loginfailed} = 1; 236 } elsif ($pass =~ m|^\$1\$([A-Za-z0-9/.]+)\$|) { 237 # native passwords (crypt-md5) 238 $webvar{loginfailed} = 1 if $pass ne unix_md5_crypt($webvar{password},$1); 239 } elsif ($pass =~ /^[0-9a-f]{32}$/) { 240 # VegaDNS import (hex-coded MD5) 241 $webvar{loginfailed} = 1 if $pass ne md5_hex($webvar{password}); 242 } else { 243 # plaintext (convenient now and then) 244 $webvar{loginfailed} = 1 if $pass ne $webvar{password}; 245 } 233 234 my $userdata = login($dbh, $webvar{username}, $webvar{password}); 235 236 if ($userdata) { 246 237 247 238 # set session bits 248 $session->param('logingroup',$ gid);249 $session->param('curgroup',$ gid);250 $session->param('uid',$u id);239 $session->param('logingroup',$userdata->{group_id}); 240 $session->param('curgroup',$userdata->{group_id}); 241 $session->param('uid',$userdata->{user_id}); 251 242 $session->param('username',$webvar{username}); 252 243 253 changepage(page => "domlist") if !defined($webvar{loginfailed});244 changepage(page => "domlist"); 254 245 255 246 } else { … … 299 290 } # handle global webvar{action}s 300 291 301 initPermissions($dbh,$session->param('uid')); 292 # finally check if the user was disabled. we could just leave this for logout/session expiry, 293 # but if they keep the session active they'll continue to have access long after being disabled. :/ 294 # Treat it as a session expiry. 295 if ($session->param('uid') && !userStatus($dbh, $session->param('uid')) ) { 296 $sid = ''; 297 $session->delete; # force expiry of the session Right Away 298 $session->flush; # make sure it hits storage 299 changepage(page=> "login", sessexpired => 1); 300 } 301 302 # Misc Things To Do on most pages 303 initPermissions($dbh, $session->param('uid')); 304 initActionLog($dbh, $session->param('uid')); 302 305 303 306 $page->param(sid => $sid) unless $webvar{page} eq 'login'; # no session ID on the login page … … 307 310 $page->param(loginfailed => 1) if $webvar{loginfailed}; 308 311 $page->param(sessexpired => 1) if $webvar{sessexpired}; 309 # $page->param(orgname => $config{orgname}) if $config{orgname} ne 'Example Corp';310 312 $page->param(version => $DNSDB::VERSION); 311 313 … … 316 318 # hmm. seeing problems in some possibly-not-so-corner cases. 317 319 # this currently only handles "domain on", "domain off" 318 if (defined($webvar{ domstatus})) {320 if (defined($webvar{zonestatus})) { 319 321 # security check - does the user have permission to access this entity? 320 322 my $flag = 0; … … 323 325 } 324 326 if ($flag && ($permissions{admin} || $permissions{domain_edit})) { 325 my $stat = domStatus($dbh,$webvar{id},$webvar{domstatus}); 326 ##fixme switch to more consise "Enabled <domain"/"Disabled <domain>" as with users? 327 logaction($webvar{id}, $session->param("username"), 328 parentID($dbh, (id => $webvar{id}, type => 'domain', revrec => $webvar{revrec})), 329 "Changed ".domainName($dbh, $webvar{id})." state to ".($stat ? 'active' : 'inactive')); 330 $page->param(resultmsg => "Changed ".domainName($dbh, $webvar{id})." state to ". 331 ($stat ? 'active' : 'inactive')); 327 my $stat = zoneStatus($dbh,$webvar{id},'n',$webvar{zonestatus}); 328 $page->param(resultmsg => $DNSDB::resultstr); 332 329 } else { 333 330 $page->param(errmsg => "You are not permitted to view or change the requested domain"); 334 331 } 335 $uri_self =~ s/\&domstatus=[^&]*//g; # clean up URL for stuffing into templates 336 } 337 338 if ($session->param('resultmsg')) { 339 $page->param(resultmsg => $session->param('resultmsg')); 340 $session->clear('resultmsg'); 341 } 342 if ($session->param('errmsg')) { 343 $page->param(errmsg => $session->param('errmsg')); 344 $session->clear('errmsg'); 345 } 332 $uri_self =~ s/\&zonestatus=[^&]*//g; # clean up URL for stuffing into templates 333 } 334 335 show_msgs(); 346 336 347 337 $page->param(curpage => $webvar{page}); … … 354 344 unless ($permissions{admin} || $permissions{domain_create}); 355 345 356 fill_grouplist("grouplist"); 346 $webvar{group} = $curgroup if !$webvar{group}; 347 fill_grouplist("grouplist", $webvar{group}); 348 fill_loclist(); 357 349 358 350 if ($session->param('add_failed')) { … … 362 354 $session->clear('errmsg'); 363 355 $page->param(domain => $webvar{domain}); 356 $page->param(addinactive => $webvar{makeactive} eq 'n'); 364 357 } 365 358 … … 379 372 $webvar{makeactive} = 0 if !defined($webvar{makeactive}); 380 373 381 my ($code,$msg) = addDomain($dbh,$webvar{domain},$webvar{group},($webvar{makeactive} eq 'on' ? 1 : 0), 382 (username => $session->param("username"), id => $session->param("uid"))); 374 my ($code,$msg) = addDomain($dbh,$webvar{domain},$webvar{group},($webvar{makeactive} eq 'on' ? 1 : 0)); 383 375 384 376 if ($code eq 'OK') { … … 388 380 changepage(page => "reclist", id => $msg); 389 381 } else { 390 logaction(0, $session->param("username"), $webvar{group}, "Failed adding domain $webvar{domain} ($msg)")391 if $config{log_failures};392 382 $session->param('add_failed', 1); 393 383 ##fixme: domain a security risk for XSS? 394 ##fixme: keep active/inactive state, group selection 395 changepage(page => "newdomain", domain => $webvar{domain}, errmsg => $msg);384 changepage(page => "newdomain", domain => $webvar{domain}, errmsg => $msg, 385 makeactive => ($webvar{makeactive} ? 'y' : 'n'), group => $webvar{group}); 396 386 } 397 387 … … 416 406 } elsif ($webvar{del} eq 'ok') { 417 407 my $pargroup = parentID($dbh, (id => $webvar{id}, type => 'domain', revrec => $webvar{revrec})); 418 my $dom = domainName($dbh, $webvar{id}); 419 my ($code,$msg) = delDomain($dbh, $webvar{id}); 408 my ($code,$msg) = delZone($dbh, $webvar{id}, $webvar{revrec}); 420 409 if ($code eq 'OK') { 421 logaction($webvar{id}, $session->param("username"), $pargroup, "Deleted domain $dom"); 422 changepage(page => "domlist", resultmsg => "Deleted domain $dom"); 410 changepage(page => "domlist", resultmsg => $msg); 423 411 } else { 424 logaction($webvar{id}, $session->param("username"), $pargroup, "Failed to delete domain $dom ($msg)") 425 if $config{log_failures}; 426 changepage(page => "domlist", errmsg => "Error deleting domain $dom: $msg"); 412 changepage(page => "domlist", errmsg => $msg); 427 413 } 428 414 … … 435 421 436 422 $webvar{revrec} = 'y'; 423 424 if (defined($webvar{zonestatus})) { 425 # security check - does the user have permission to access this entity? 426 my $flag = 0; 427 foreach (@viewablegroups) { 428 $flag = 1 if isParent($dbh, $_, 'group', $webvar{id}, 'revzone'); 429 } 430 if ($flag && ($permissions{admin} || $permissions{domain_edit})) { 431 my $stat = zoneStatus($dbh,$webvar{id},'y',$webvar{zonestatus}); 432 $page->param(resultmsg => $DNSDB::resultstr); 433 } else { 434 $page->param(errmsg => "You are not permitted to view or change the requested reverse zone"); 435 } 436 $uri_self =~ s/\&zonestatus=[^&]*//g; # clean up URL for stuffing into templates 437 } 438 439 show_msgs(); 440 437 441 $page->param(curpage => $webvar{page}); 438 442 listzones(); … … 446 450 fill_grouplist("grouplist"); 447 451 448 if ($webvar{add_failed}) { 449 $page->param(add_failed => 1); 450 $page->param(errmsg => $webvar{errmsg}); 452 # prepopulate revpatt with the matching default record 453 # getRecByName($dbh, (revrec => $webvar{revrec}, defrec => $webvar{defrec}, host => 'string')); 454 455 if ($session->param('add_failed')) { 456 $session->clear('add_failed'); 457 $page->param(errmsg => $session->param('errmsg')); 458 $session->clear('errmsg'); 451 459 $page->param(revzone => $webvar{revzone}); 452 460 $page->param(revpatt => $webvar{revpatt}); … … 465 473 466 474 my ($code,$msg) = addRDNS($dbh, $webvar{revzone}, $webvar{revpatt}, $webvar{group}, 467 ($webvar{makeactive} eq 'on' ? 1 : 0), 468 (username => $session->param("username"), id => $session->param("uid")) ); 475 ($webvar{makeactive} eq 'on' ? 1 : 0)); 469 476 470 477 if ($code eq 'OK') { 471 logaction(0, $session->param("username"), $webvar{group}, "Added reverse zone $webvar{revzone}", $msg);472 478 changepage(page => "reclist", id => $msg, revrec => 'y'); 479 } elsif ($code eq 'WARN') { 480 changepage(page => "reclist", id => $msg, revrec => 'y', warnmsg => $DNSDB::resultstr); 473 481 } else { 474 logaction(0, $session->param("username"), $webvar{group}, "Failed adding reverse zone $webvar{revzone} ($msg)"); 475 changepage(page => "newrevzone", add_failed => 1, revzone => $webvar{revzone}, revpatt => $webvar{revpatt}, 476 errmsg => $msg); 477 } 478 479 #} elsif ($webvar{page} eq 'delrevzone') { 482 $session->param('add_failed', 1); 483 changepage(page => "newrevzone", revzone => $webvar{revzone}, revpatt => $webvar{revpatt}, errmsg => $msg); 484 } 485 486 } elsif ($webvar{page} eq 'delrevzone') { 487 488 changepage(page => "revzones", errmsg => "You are not permitted to delete reverse zones") 489 unless ($permissions{admin} || $permissions{domain_delete}); 490 491 # security check - does the user have permission to access this entity? 492 if (!check_scope(id => $webvar{id}, type => 'revzone')) { 493 changepage(page => "revzones", errmsg => "You do not have permission to delete the requested reverse zone"); 494 } 495 496 $page->param(id => $webvar{id}); 497 498 # first pass = confirm y/n (sorta) 499 if (!defined($webvar{del})) { 500 501 $page->param(del_getconf => 1); 502 $page->param(revzone => revName($dbh,$webvar{id})); 503 504 } elsif ($webvar{del} eq 'ok') { 505 my $pargroup = parentID($dbh, (id => $webvar{id}, type => 'revzone', revrec => $webvar{revrec})); 506 my $zone = revName($dbh, $webvar{id}); 507 my ($code,$msg) = delZone($dbh, $webvar{id}, 'y'); 508 if ($code eq 'OK') { 509 changepage(page => "revzones", resultmsg => $msg); 510 } else { 511 changepage(page => "revzones", errmsg => $msg); 512 } 513 514 } else { 515 # cancelled. whee! 516 changepage(page => "revzones"); 517 } 480 518 481 519 } elsif ($webvar{page} eq 'reclist') { … … 531 569 distance => 'Distance', weight => 'Weight', port => 'Port', ttl => 'TTL'); 532 570 } else { 533 @cols = (' host', 'type', 'val', 'ttl');534 %colheads = ( host => 'IP Address', type => 'Type', val=> 'Hostname', ttl => 'TTL');571 @cols = ('val', 'type', 'host', 'ttl'); 572 %colheads = (val => 'IP Address', type => 'Type', host => 'Hostname', ttl => 'TTL'); 535 573 } 536 574 my %custom = (id => $webvar{id}, defrec => $webvar{defrec}, revrec => $webvar{revrec}); … … 547 585 showzone($webvar{defrec}, $webvar{revrec}, $webvar{id}); 548 586 if ($webvar{defrec} eq 'n') { 549 # showzone('n',$webvar{id});550 ##fixme: permission for viewing logs?551 ##fixme: determine which slice of the log we view (group, domain, revzone)552 587 if ($webvar{revrec} eq 'n') { 553 588 $page->param(logdom => 1); … … 557 592 } 558 593 559 if ($session->param('resultmsg')) { 560 $page->param(resultmsg => $session->param('resultmsg')); 561 $session->clear('resultmsg'); 562 } 563 if ($session->param('warnmsg')) { 564 $page->param(warnmsg => $session->param('warnmsg')); 565 $session->clear('warnmsg'); 566 } 567 if ($session->param('errmsg')) { 568 $page->param(errmsg => $session->param('errmsg')); 569 $session->clear('errmsg'); 570 } 594 show_msgs(); 571 595 572 596 } # close "you can't edit default records" check … … 608 632 fill_recdata(); 609 633 634 if ($webvar{defrec} eq 'n') { 635 my $defloc = getZoneLocation($dbh, $webvar{revrec}, $webvar{parentid}); 636 fill_loclist($curgroup, $defloc); 637 } 638 610 639 } elsif ($webvar{recact} eq 'add') { 611 640 … … 613 642 unless ($permissions{admin} || $permissions{record_create}); 614 643 644 # location check - if user does not have record_locchg, set $webvar{location} to default location for zone 645 my $parloc = getZoneLocation($dbh, $webvar{revrec}, $webvar{parentid}); 646 $webvar{location} = $parloc unless ($permissions{admin} || $permissions{record_locchg}); 647 615 648 my @recargs = ($dbh,$webvar{defrec},$webvar{revrec},$webvar{parentid}, 616 \$webvar{name},\$webvar{type},\$webvar{address},$webvar{ttl} );649 \$webvar{name},\$webvar{type},\$webvar{address},$webvar{ttl},$webvar{location}); 617 650 if ($webvar{type} == $reverse_typemap{MX} or $webvar{type} == $reverse_typemap{SRV}) { 618 651 push @recargs, $webvar{distance}; … … 626 659 627 660 if ($code eq 'OK' || $code eq 'WARN') { 628 my $restr;629 if ($webvar{defrec} eq 'y') {630 $restr = "Added default record '$webvar{name} $typemap{$webvar{type}}";631 $restr .= " [distance $webvar{distance}]" if $typemap{$webvar{type}} eq 'MX';632 $restr .= " [priority $webvar{distance}] [weight $webvar{weight}] [port $webvar{port}]"633 if $typemap{$webvar{type}} eq 'SRV';634 $restr .= " $webvar{address}', TTL $webvar{ttl}";635 logaction(0, $session->param("username"), $webvar{parentid}, $restr);636 } else {637 $restr = "Added record '$webvar{name} $typemap{$webvar{type}}";638 $restr .= " [distance $webvar{distance}]" if $typemap{$webvar{type}} eq 'MX';639 $restr .= " [priority $webvar{distance}] [weight $webvar{weight}] [port $webvar{port}]"640 if $typemap{$webvar{type}} eq 'SRV';641 $restr .= " $webvar{address}', TTL $webvar{ttl}";642 logaction($webvar{parentid}, $session->param("username"),643 parentID($dbh, (id => $webvar{parentid}, type => 'domain', revrec => $webvar{revrec})), $restr);644 }645 661 my %pageparams = (page => "reclist", id => $webvar{parentid}, 646 662 defrec => $webvar{defrec}, revrec => $webvar{revrec}); 647 $pageparams{warnmsg} = $msg."<br><br>\n".$ restr if $code eq 'WARN';648 $pageparams{resultmsg} = $ restr if $code eq 'OK';663 $pageparams{warnmsg} = $msg."<br><br>\n".$DNSDB::resultstr if $code eq 'WARN'; 664 $pageparams{resultmsg} = $DNSDB::resultstr if $code eq 'OK'; 649 665 changepage(%pageparams); 650 666 } else { … … 657 673 $page->param(id => $webvar{id}); 658 674 fill_recdata(); # populate the form... er, mostly. 659 if ($config{log_failures}) { 660 if ($webvar{defrec} eq 'y') { 661 logaction(0, $session->param("username"), $webvar{parentid}, 662 "Failed adding default record '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl} ($msg)"); 663 } else { 664 logaction($webvar{parentid}, $session->param("username"), 665 parentID($dbh, (id => $webvar{parentid}, type => 'domain', revrec => $webvar{revrec})), 666 "Failed adding record '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl} ($msg)"); 667 } 675 if ($webvar{defrec} eq 'n') { 676 fill_loclist($curgroup, $webvar{location}); 668 677 } 669 678 } … … 685 694 $page->param(port => $recdata->{port}); 686 695 $page->param(ttl => $recdata->{ttl}); 687 $page->param(typelist => getTypelist($dbh, $webvar{revrec}, $webvar{type})); 696 $page->param(typelist => getTypelist($dbh, $webvar{revrec}, $recdata->{type})); 697 698 if ($webvar{defrec} eq 'n') { 699 fill_loclist($curgroup, $recdata->{location}); 700 } 688 701 689 702 } elsif ($webvar{recact} eq 'update') { … … 692 705 unless ($permissions{admin} || $permissions{record_edit}); 693 706 694 # prevent out-of-domain records from getting added by appending the domain, or DOMAIN for default records 695 my $pname = ($webvar{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$webvar{parentid})); 696 $webvar{name} =~ s/\s+$//; 697 $webvar{name} =~ s/\.*$/\.$pname/ if $webvar{name} !~ /$pname$/; 698 699 # get current/previous record info so we can log "updated 'foo A 1.2.3.4' to 'foo A 2.3.4.5'" 707 # retain old location if user doesn't have permission to fiddle locations 700 708 my $oldrec = getRecLine($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id}); 701 702 my ($code,$msg) = updateRec($dbh,$webvar{defrec},$webvar{id}, 703 $webvar{name},$webvar{type},$webvar{address},$webvar{ttl}, 709 $webvar{location} = $oldrec->{location} unless ($permissions{admin} || $permissions{record_locchg}); 710 711 my ($code,$msg) = updateRec($dbh,$webvar{defrec},$webvar{revrec},$webvar{id},$webvar{parentid}, 712 \$webvar{name},\$webvar{type},\$webvar{address},$webvar{ttl},$webvar{location}, 704 713 $webvar{distance},$webvar{weight},$webvar{port}); 705 714 706 if ($code eq 'OK') { 707 ##fixme: retrieve old record info for full logging of change 708 if ($webvar{defrec} eq 'y') { 709 my $restr = "Updated default record from '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl}\n". 710 "to '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl}"; 711 logaction(0, $session->param("username"), $webvar{parentid}, $restr); 712 changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr); 713 } else { 714 my $restr = "Updated record from '$oldrec->{host} $typemap{$oldrec->{type}} $oldrec->{val}', TTL $oldrec->{ttl}\n". 715 "to '$webvar{name} $typemap{$webvar{type}} $webvar{address}', TTL $webvar{ttl}"; 716 logaction($webvar{parentid}, $session->param("username"), 717 parentID($dbh, (id => $webvar{id}, type => 'record', defrec => $webvar{defrec}, 718 revrec => $webvar{revrec}, partype => 'group')), 719 $restr); 720 changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, resultmsg => $restr); 721 } 715 if ($code eq 'OK' || $code eq 'WARN') { 716 my %pageparams = (page => "reclist", id => $webvar{parentid}, 717 defrec => $webvar{defrec}, revrec => $webvar{revrec}); 718 $pageparams{warnmsg} = $msg."<br><br>\n".$DNSDB::resultstr if $code eq 'WARN'; 719 $pageparams{resultmsg} = $DNSDB::resultstr if $code eq 'OK'; 720 changepage(%pageparams); 722 721 } else { 723 722 $page->param(failed => 1); … … 729 728 $page->param(id => $webvar{id}); 730 729 fill_recdata(); 731 if ($config{log_failures}) {732 if ($webvar{defrec} eq 'y') {733 logaction(0, $session->param("username"), $webvar{parentid},734 "Failed updating default record '$typemap{$webvar{type}} $webvar{name} $webvar{address}', TTL $webvar{ttl} ($msg)");735 } else {736 logaction($webvar{parentid}, $session->param("username"),737 parentID($dbh, (id => $webvar{parentid}, type => 'domain', revrec => $webvar{revrec})),738 "Failed updating record '$typemap{$webvar{type}} $webvar{name} $webvar{address}', TTL $webvar{ttl} ($msg)");739 }740 }741 730 } 742 731 } … … 745 734 $page->param(dohere => "default records in group ".groupName($dbh,$webvar{parentid})); 746 735 } else { 747 $page->param(parentid => $webvar{parentid});748 736 $page->param(dohere => domainName($dbh,$webvar{parentid})) if $webvar{revrec} eq 'n'; 749 737 $page->param(dohere => revName($dbh,$webvar{parentid})) if $webvar{revrec} eq 'y'; … … 781 769 $page->param(recval => $rec->{val}); 782 770 } elsif ($webvar{del} eq 'ok') { 783 # get rec data before we try to delete it784 my $rec = getRecLine($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id});785 771 my ($code,$msg) = delRec($dbh, $webvar{defrec}, $webvar{revrec}, $webvar{id}); 786 772 if ($code eq 'OK') { 787 if ($webvar{defrec} eq 'y') { 788 my $recclass = ($webvar{revrec} eq 'n' ? 'default record' : 'default reverse record'); 789 ##fixme: log distance for MX; log port/weight/distance for SRV 790 my $restr = "Deleted $recclass '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl}"; 791 logaction(0, $session->param("username"), $rec->{parid}, $restr); 792 changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, 793 revrec => $webvar{revrec}, resultmsg => $restr); 794 } else { 795 my $recclass = ($webvar{revrec} eq 'n' ? 'record' : 'reverse record'); 796 my $restr = "Deleted $recclass '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl}"; 797 logaction($rec->{parid}, $session->param("username"), 798 parentID($dbh, (id => $rec->{parid}, type => 'domain', revrec => $webvar{revrec})), 799 $restr); 800 changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, 801 revrec => $webvar{revrec}, resultmsg => $restr); 802 } 773 changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, 774 revrec => $webvar{revrec}, resultmsg => $msg); 803 775 } else { 804 776 ## need to find failure mode 805 if ($config{log_failures}) {806 if ($webvar{defrec} eq 'y') {807 logaction(0, $session->param("username"), $rec->{parid},808 "Failed deleting default record '$rec->{host} $typemap{$rec->{type}} $rec->{val}',".809 " TTL $rec->{ttl} ($msg)");810 } else {811 logaction($rec->{parid}, $session->param("username"),812 parentID($dbh, (id => $rec->{parid}, type => 'domain', revrec => $webvar{revrec})),813 "Failed deleting record '$rec->{host} $typemap{$rec->{type}} $rec->{val}', TTL $rec->{ttl} ($msg)");814 }815 }816 777 changepage(page => "reclist", id => $webvar{parentid}, defrec => $webvar{defrec}, 817 revrec => $webvar{revrec}, errmsg => "Error deleting record: $msg");778 revrec => $webvar{revrec}, errmsg => $msg); 818 779 } 819 780 } else { … … 840 801 } 841 802 842 fillsoa($webvar{defrec},$webvar{ id});803 fillsoa($webvar{defrec},$webvar{revrec},$webvar{id}); 843 804 844 805 } elsif ($webvar{page} eq 'updatesoa') { … … 848 809 if (!check_scope(id => $webvar{recid}, type => 849 810 ($webvar{defrec} eq 'y' ? ($webvar{revrec} eq 'y' ? 'defrevrec' : 'defrec') : 'record'))) { 811 ##fixme: should we redirect to the requested record list page instead of the domain list? 850 812 changepage(page => 'domlist', errmsg => "You do not have permission to edit the requested SOA record"); 851 813 } … … 853 815 if (!check_scope(id => $webvar{id}, type => 854 816 ($webvar{defrec} eq 'y' ? 'group' : ($webvar{revrec} eq 'y' ? 'revzone' : 'domain')))) { 855 changepage(page => 'domlist', errmsg => "You do not have permission to edit the ". 817 changepage(page => ($webvar{revrec} eq 'y' ? 'revzones' : 'domlist'), 818 errmsg => "You do not have permission to edit the ". 856 819 ($webvar{defrec} eq 'y' ? 'default ' : '')."SOA record for the requested ". 857 ($webvar{defrec} eq 'y' ? 'group' : 'domain'));820 ($webvar{defrec} eq 'y' ? 'group' : ($webvar{revrec} eq 'y' ? 'reverse zone' : 'domain')) ); 858 821 } 859 822 … … 861 824 unless ($permissions{admin} || $permissions{domain_edit}); 862 825 863 # get old SOA for log 864 my %soa = getSOA($dbh,$webvar{defrec},$webvar{id}); 865 866 my $sth; 867 ##fixme: push SQL into DNSDB.pm 868 ##fixme: data validation: make sure {recid} is really the SOA for {id} 869 # no domain ID, so we're editing the default SOA for a group (we don't care which one here) 870 # plus a bit of magic to update the appropriate table 871 my $sql = "UPDATE ".($webvar{defrec} eq 'y' ? "default_records" : "records"). 872 " SET host=?, val=?, ttl=? WHERE record_id=?"; 873 $sth = $dbh->prepare($sql); 874 $sth->execute("$webvar{contact}:$webvar{prins}", 875 "$webvar{refresh}:$webvar{retry}:$webvar{expire}:$webvar{minttl}", 876 $webvar{ttl}, 877 $webvar{recid}); 878 879 if ($sth->err) { 826 my ($code, $msg) = updateSOA($dbh, $webvar{defrec}, $webvar{revrec}, 827 (contact => $webvar{contact}, prins => $webvar{prins}, refresh => $webvar{refresh}, 828 retry => $webvar{retry}, expire => $webvar{expire}, minttl => $webvar{minttl}, 829 ttl => $webvar{ttl}, id => $webvar{id}) ); 830 if ($code eq 'OK') { 831 changepage(page => "reclist", id => $webvar{id}, defrec => $webvar{defrec}, revrec => $webvar{revrec}, 832 resultmsg => "SOA record updated"); 833 } else { 880 834 $page->param(update_failed => 1); 881 $page->param(msg => $DBI::errstr); 882 fillsoa($webvar{defrec},$webvar{id}); 883 ##fixme: faillog 884 } else { 885 886 # do this in the order of "default to most common case" 887 my $loggroup; 888 my $logdomain = $webvar{id}; 889 if ($webvar{defrec} eq 'y') { 890 $loggroup = $webvar{id}; 891 $logdomain = 0; 892 } else { 893 $loggroup = parentID($dbh, (id => $logdomain, type => 'domain', revrec => $webvar{revrec})); 894 } 895 896 logaction($logdomain, $session->param("username"), $loggroup, 897 "Updated ".($webvar{defrec} eq 'y' ? 'default ' : '')."SOA for ". 898 ($webvar{defrec} eq 'y' ? groupName($dbh, $webvar{id}) : domainName($dbh, $webvar{id}) ). 899 ": (ns $soa{prins}, contact $soa{contact}, refresh $soa{refresh},". 900 " retry $soa{retry}, expire $soa{expire}, minTTL $soa{minttl}, TTL $soa{ttl}) to ". 901 "(ns $webvar{prins}, contact $webvar{contact}, refresh $webvar{refresh},". 902 " retry $webvar{retry}, expire $webvar{expire}, minTTL $webvar{minttl}, TTL $webvar{ttl})"); 903 changepage(page => "reclist", id => $webvar{id}, defrec => $webvar{defrec}, 904 resultmsg => "SOA record updated"); 835 $page->param(msg => $msg); 836 fillsoa($webvar{defrec}, $webvar{revrec}, $webvar{id}, 'w'); 905 837 } 906 838 … … 914 846 $page->param(delgrp => $permissions{admin} || $permissions{group_delete}); 915 847 916 if ($session->param('resultmsg')) { 917 $page->param(resultmsg => $session->param('resultmsg')); 918 $session->clear('resultmsg'); 919 } 920 if ($session->param('warnmsg')) { 921 $page->param(warnmsg => $session->param('warnmsg')); 922 $session->clear('warnmsg'); 923 } 924 if ($session->param('errmsg')) { 925 $page->param(errmsg => $session->param('errmsg')); 926 $session->clear('errmsg'); 927 } 848 show_msgs(); 928 849 $page->param(curpage => $webvar{page}); 929 850 … … 951 872 } 952 873 } 953 # force inheritance of parent group's default records with inherit flag, 954 # otherwise we end up with the hardcoded defaults from DNSDB.pm. See 955 # https://secure.deepnet.cx/trac/dnsadmin/ticket/8 for the UI enhancement 956 # that will make this variable. 957 my ($code,$msg) = addGroup($dbh, $webvar{newgroup}, $webvar{pargroup}, \%newperms, 1); 874 # "Chained" permissions. Some permissions imply others; make sure they get set. 875 foreach (keys %permchains) { 876 if ($newperms{$_} && !$newperms{$permchains{$_}}) { 877 $newperms{$permchains{$_}} = 1; 878 } 879 } 880 # not gonna provide the 4th param: template-or-clone flag, just yet 881 my ($code,$msg) = addGroup($dbh, $webvar{newgroup}, $webvar{pargroup}, \%newperms); 958 882 if ($code eq 'OK') { 959 logaction(0, $session->param("username"), $webvar{pargroup}, "Added group $webvar{newgroup}");960 883 if ($alterperms) { 961 884 changepage(page => "grpman", warnmsg => … … 965 888 } 966 889 } # fallthrough else 967 logaction(0, $session->param("username"), $webvar{pargroup}, "Failed to add group $webvar{newgroup}: $msg")968 if $config{log_failures};969 890 # no point in doing extra work 970 891 fill_permissions($page, \%newperms); … … 1001 922 1002 923 } elsif ($webvar{del} eq 'ok') { 1003 my $deleteme = groupName($dbh,$webvar{id}); # get this before we delete it...1004 my $delparent = parentID($dbh, (id => $webvar{id}, type => 'group'));1005 924 my ($code,$msg) = delGroup($dbh, $webvar{id}); 1006 925 if ($code eq 'OK') { 1007 926 ##fixme: need to clean up log when deleting a major container 1008 logaction(0, $session->param("username"), $delparent, "Deleted group $deleteme"); 1009 changepage(page => "grpman", resultmsg => "Deleted group $deleteme"); 927 changepage(page => "grpman", resultmsg => $msg); 1010 928 } else { 1011 929 # need to find failure mode 1012 logaction(0, $session->param("username"), $delparent, "Failed to delete group $deleteme: $msg") 1013 if $config{log_failures}; 1014 changepage(page => "grpman", errmsg => "Error deleting group $deleteme: $msg"); 930 changepage(page => "grpman", errmsg => $msg); 1015 931 } 1016 932 } else { … … 1030 946 } 1031 947 1032 if ($webvar{grpaction} eq 'updperms') {948 if ($webvar{grpaction} && $webvar{grpaction} eq 'updperms') { 1033 949 # extra safety check; make sure user can't construct a URL to bypass ACLs 1034 950 my %curperms; … … 1046 962 } 1047 963 } 964 # "Chained" permissions. Some permissions imply others; make sure they get set. 965 foreach (keys %permchains) { 966 if ($chperms{$_} && !$chperms{$permchains{$_}}) { 967 $chperms{$permchains{$_}} = 1; 968 } 969 } 1048 970 my ($code,$msg) = changePermissions($dbh, 'group', $webvar{gid}, \%chperms); 1049 971 if ($code eq 'OK') { 1050 logaction(0, $session->param("username"), $webvar{gid},1051 "Updated default permissions in group $webvar{gid} (".groupName($dbh, $webvar{gid}).")");1052 972 if ($alterperms) { 1053 973 changepage(page => "grpman", warnmsg => … … 1055 975 groupName($dbh, $webvar{gid})." updated with reduced access"); 1056 976 } else { 1057 changepage(page => "grpman", resultmsg => 1058 "Updated default permissions in group ".groupName($dbh, $webvar{gid})); 977 changepage(page => "grpman", resultmsg => $msg); 1059 978 } 1060 979 } # fallthrough else 1061 logaction(0, $session->param("username"), $webvar{gid}, "Failed to update default permissions in group ".1062 groupName($dbh, $webvar{gid}).": $msg")1063 if $config{log_failures};1064 980 # no point in doing extra work 1065 981 fill_permissions($page, \%chperms); … … 1074 990 } elsif ($webvar{page} eq 'bulkdomain') { 1075 991 # Bulk operations on domains. Note all but group move are available on the domain list. 992 ##fixme: do we care about bulk operations on revzones? Move-to-group, activate, deactivate, 993 # and delete should all be much rarer for revzones than for domains. 1076 994 1077 995 changepage(page => "domlist", errmsg => "You are not permitted to make bulk domain changes") … … 1080 998 fill_grouplist("grouplist"); 1081 999 1082 ##fixme 1083 ##fixme push the SQL and direct database fiddling off into a sub in DNSDB.pm 1084 ##fixme 1085 1086 my $sth = $dbh->prepare("SELECT count(*) FROM domains WHERE group_id=?"); 1087 $sth->execute($curgroup); 1088 my ($count) = ($sth->fetchrow_array); 1000 my $count = getZoneCount($dbh, (revrec => 'n', curgroup => $curgroup) ); 1089 1001 1090 1002 $page->param(curpage => $webvar{page}); … … 1093 1005 $page->param(perpage => $perpage); 1094 1006 1095 my @domlist; 1096 my $sql = "SELECT domain_id,domain FROM domains". 1097 " WHERE group_id=?". 1098 " ORDER BY domain". 1099 ($offset eq 'all' ? '' : " LIMIT $perpage OFFSET ".$offset*$perpage); 1100 $sth = $dbh->prepare($sql); 1101 $sth->execute($curgroup); 1007 my $domlist = getZoneList($dbh, (revrec => 'n', curgroup => $curgroup) ); 1102 1008 my $rownum = 0; 1103 while (my @data = $sth->fetchrow_array) { 1104 my %row; 1105 $row{domid} = $data[0]; 1106 $row{domain} = $data[1]; 1107 $rownum++; # putting this in the expression below causes failures. *eyeroll* 1108 $row{newrow} = $rownum % 5 == 0; 1109 push @domlist, \%row; 1110 } 1111 $page->param(domtable => \@domlist); 1009 foreach my $dom (@{$domlist}) { 1010 delete $dom->{status}; 1011 delete $dom->{group}; 1012 $dom->{newrow} = (++$rownum) % 5 == 0; 1013 } 1014 1015 $page->param(domtable => $domlist); 1112 1016 # ACLs 1113 1017 $page->param(maymove => ($permissions{admin} || ($permissions{domain_edit} && $permissions{domain_create} && $permissions{domain_delete}))); … … 1123 1027 } 1124 1028 1029 # per-action scope checks 1125 1030 if ($webvar{bulkaction} eq 'move') { 1126 1031 changepage(page => "domlist", errmsg => "You are not permitted to bulk-move domains") … … 1128 1033 my $newgname = groupName($dbh,$webvar{destgroup}); 1129 1034 $page->param(action => "Move to group $newgname"); 1130 my @bulkresults;1131 # nngh. due to alpha-sorting on the previous page, we can't use domid-numeric1132 # order here, and since we don't have the domain names until we go around this1133 # loop, we can't alpha-sort them here. :(1134 foreach (keys %webvar) {1135 my %row;1136 next unless $_ =~ /^dom_\d+$/;1137 # second security check - does the user have permission to meddle with this domain?1138 if (!check_scope(id => $webvar{$_}, type => 'domain')) {1139 $row{domerr} = "You are not permitted to make changes to the requested domain";1140 $row{domain} = $webvar{$_};1141 push @bulkresults, \%row;1142 next;1143 }1144 $row{domain} = domainName($dbh,$webvar{$_});1145 my ($code, $msg) = changeGroup($dbh, 'domain', $webvar{$_}, $webvar{destgroup});1146 if ($code eq 'OK') {1147 logaction($webvar{$_}, $session->param("username"),1148 parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})),1149 "Moved domain ".domainName($dbh, $webvar{$_})." to group $newgname");1150 $row{domok} = ($code eq 'OK');1151 } else {1152 logaction($webvar{$_}, $session->param("username"),1153 parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})),1154 "Failed to move domain ".domainName($dbh, $webvar{$_})." to group $newgname: $msg")1155 if $config{log_failures};1156 }1157 $row{domerr} = $msg;1158 push @bulkresults, \%row;1159 }1160 $page->param(bulkresults => \@bulkresults);1161 1162 1035 } elsif ($webvar{bulkaction} eq 'deactivate' || $webvar{bulkaction} eq 'activate') { 1163 1036 changepage(page => "domlist", errmsg => "You are not permitted to bulk-$webvar{bulkaction} domains") 1164 1037 unless ($permissions{admin} || $permissions{domain_edit}); 1165 1038 $page->param(action => "$webvar{bulkaction} domains"); 1166 my @bulkresults;1167 foreach (keys %webvar) {1168 my %row;1169 next unless $_ =~ /^dom_\d+$/;1170 # second security check - does the user have permission to meddle with this domain?1171 if (!check_scope(id => $webvar{$_}, type => 'domain')) {1172 $row{domerr} = "You are not permitted to make changes to the requested domain";1173 $row{domain} = $webvar{$_};1174 push @bulkresults, \%row;1175 next;1176 }1177 $row{domain} = domainName($dbh,$webvar{$_});1178 ##fixme: error handling on status change1179 my $stat = domStatus($dbh,$webvar{$_},($webvar{bulkaction} eq 'activate' ? 'domon' : 'domoff'));1180 logaction($webvar{$_}, $session->param("username"),1181 parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})),1182 "Changed domain ".domainName($dbh, $webvar{$_})." state to ".($stat ? 'active' : 'inactive'));1183 $row{domok} = 1;1184 # $row{domok} = ($code eq 'OK');1185 # $row{domerr} = $msg;1186 push @bulkresults, \%row;1187 }1188 $page->param(bulkresults => \@bulkresults);1189 1190 1039 } elsif ($webvar{bulkaction} eq 'delete') { 1191 1040 changepage(page => "domlist", errmsg => "You are not permitted to bulk-delete domains") 1192 1041 unless ($permissions{admin} || $permissions{domain_delete}); 1193 1042 $page->param(action => "$webvar{bulkaction} domains"); 1194 my @bulkresults; 1195 foreach (keys %webvar) { 1196 my %row; 1197 next unless $_ =~ /^dom_\d+$/; 1198 # second security check - does the user have permission to meddle with this domain? 1199 if (!check_scope(id => $webvar{$_}, type => 'domain')) { 1200 $row{domerr} = "You are not permitted to make changes to the requested domain"; 1201 $row{domain} = $webvar{$_}; 1202 push @bulkresults, \%row; 1203 next; 1204 } 1205 $row{domain} = domainName($dbh,$webvar{$_}); 1206 my $pargroup = parentID($dbh, (id => $webvar{$_}, type => 'domain', revrec => $webvar{revrec})); 1207 my $dom = domainName($dbh, $webvar{$_}); 1208 my ($code, $msg) = delDomain($dbh, $webvar{$_}); 1209 if ($code eq 'OK') { 1210 logaction($webvar{$_}, $session->param("username"), $pargroup, "Deleted domain $dom"); 1211 $row{domok} = ($code eq 'OK'); 1212 } else { 1213 logaction($webvar{$_}, $session->param("username"), $pargroup, "Failed to delete domain $dom: $msg") 1214 if $config{log_failures}; 1215 } 1043 } else { 1044 # unknown action, bypass actually doing anything. it should not be possible in 1045 # normal operations, and anyone who meddles with the URL gets what they deserve. 1046 goto DONEBULK; 1047 } # move/(de)activate/delete if() 1048 1049 my @bulkresults; 1050 # nngh. due to alpha-sorting on the previous page, we can't use domid-numeric 1051 # order here, and since we don't have the domain names until we go around this 1052 # loop, we can't alpha-sort them here. :( 1053 foreach (keys %webvar) { 1054 my %row; 1055 next unless $_ =~ /^dom_\d+$/; 1056 # second security check - does the user have permission to meddle with this domain? 1057 if (!check_scope(id => $webvar{$_}, type => 'domain')) { 1058 $row{domerr} = "You are not permitted to make changes to the requested domain"; 1059 $row{domain} = $webvar{$_}; 1060 push @bulkresults, \%row; 1061 next; 1062 } 1063 $row{domain} = domainName($dbh,$webvar{$_}); 1064 1065 # Do the $webvar{bulkaction} 1066 my ($code, $msg); 1067 ($code, $msg) = changeGroup($dbh, 'domain', $webvar{$_}, $webvar{destgroup}) 1068 if $webvar{bulkaction} eq 'move'; 1069 if ($webvar{bulkaction} eq 'deactivate' || $webvar{bulkaction} eq 'activate') { 1070 my $stat = zoneStatus($dbh,$webvar{$_},'n',($webvar{bulkaction} eq 'activate' ? 'domon' : 'domoff')); 1071 $code = (defined($stat) ? 'OK' : 'FAIL'); 1072 $msg = (defined($stat) ? $DNSDB::resultstr : $DNSDB::errstr); 1073 } 1074 ($code, $msg) = delZone($dbh, $webvar{$_}, 'n') 1075 if $webvar{bulkaction} eq 'delete'; 1076 1077 # Set the result output from the action 1078 if ($code eq 'OK') { 1079 $row{domok} = $msg; 1080 } elsif ($code eq 'WARN') { 1081 $row{domwarn} = $msg; 1082 } else { 1216 1083 $row{domerr} = $msg; 1217 push @bulkresults, \%row; 1218 } 1219 $page->param(bulkresults => \@bulkresults); 1220 1221 } # move/(de)activate/delete if() 1222 1223 # not going to handle the unknown $webvar{action} else; it should not be possible in normal 1224 # operations, and anyone who meddles with the URL gets what they deserve. 1084 } 1085 push @bulkresults, \%row; 1086 1087 } # foreach (keys %webvar) 1088 $page->param(bulkresults => \@bulkresults); 1225 1089 1226 1090 # Yes, this is a GOTO target. PTHBTTT. … … 1235 1099 $flag = 1 if isParent($dbh, $_, 'group', $webvar{id}, 'user'); 1236 1100 } 1237 if ($flag && ($permissions{admin} || $permissions{user_edit})) { 1101 if ($flag && ($permissions{admin} || $permissions{user_edit} || 1102 ($permissions{self_edit} && $webvar{id} == $session->param('uid')) )) { 1238 1103 my $stat = userStatus($dbh,$webvar{id},$webvar{userstatus}); 1239 logaction(0, $session->param("username"), parentID($dbh, (id => $webvar{id}, type => 'user')), 1240 ($stat ? 'Enabled' : 'Disabled')." ".userFullName($dbh, $webvar{id}, '%u')); 1241 $page->param(resultmsg => ($stat ? 'Enabled' : 'Disabled')." ".userFullName($dbh, $webvar{id}, '%u')); 1104 $page->param(resultmsg => $DNSDB::resultstr); 1242 1105 } else { 1243 1106 $page->param(errmsg => "You are not permitted to view or change the requested user"); … … 1255 1118 $page->param(deluser => $permissions{admin} || $permissions{user_delete}); 1256 1119 1257 if ($session->param('resultmsg')) { 1258 $page->param(resultmsg => $session->param('resultmsg')); 1259 $session->clear('resultmsg'); 1260 } 1261 if ($session->param('warnmsg')) { 1262 $page->param(warnmsg => $session->param('warnmsg')); 1263 $session->clear('warnmsg'); 1264 } 1265 if ($session->param('errmsg')) { 1266 $page->param(errmsg => $session->param('errmsg')); 1267 $session->clear('errmsg'); 1268 } 1120 show_msgs(); 1269 1121 $page->param(curpage => $webvar{page}); 1270 1122 … … 1293 1145 $page->param(add => 1) if $webvar{useraction} eq 'add'; 1294 1146 1295 my ($code,$msg); 1147 # can't re-use $code and $msg for update if we want to be able to identify separate failure states 1148 my ($code,$code2,$msg,$msg2) = ('OK','OK','OK','OK'); 1296 1149 1297 1150 my $alterperms = 0; # flag iff we need to force custom permissions due to user's current access limits … … 1340 1193 $permstring = 'i'; 1341 1194 } 1195 # "Chained" permissions. Some permissions imply others; make sure they get set. 1196 foreach (keys %permchains) { 1197 if ($newperms{$_} && !$newperms{$permchains{$_}}) { 1198 $newperms{$permchains{$_}} = 1; 1199 $permstring .= ",$permchains{$_}"; 1200 } 1201 } 1342 1202 if ($webvar{useraction} eq 'add') { 1343 1203 changepage(page => "useradmin", errmsg => "You do not have permission to add new users") … … 1347 1207 ($webvar{makeactive} eq 'on' ? 1 : 0), $webvar{accttype}, $permstring, 1348 1208 $webvar{fname}, $webvar{lname}, $webvar{phone}); 1349 logaction(0, $session->param("username"), $curgroup, "Added user $webvar{uname} (uid $msg)")1350 if $code eq 'OK';1351 1209 } else { 1352 1210 changepage(page => "useradmin", errmsg => "You do not have permission to edit users") 1353 unless $permissions{admin} || $permissions{user_edit}; 1211 unless $permissions{admin} || $permissions{user_edit} || 1212 ($permissions{self_edit} && $session->param('uid') == $webvar{uid}); 1354 1213 # security check - does the user have permission to access this entity? 1355 1214 if (!check_scope(id => $webvar{user}, type => 'user')) { 1356 1215 changepage(page => "useradmin", errmsg => "You do not have permission to edit the requested user"); 1357 1216 } 1358 # User update is icky. I'd really like to do this in one atomic 1359 # operation, but that would duplicate a **lot** of code in DNSDB.pm 1217 # User update is icky. I'd really like to do this in one atomic operation, 1218 # but that gets hairy by either duplicating a **lot** of code in DNSDB.pm 1219 # or self-torture trying to not commit the transaction until we're really done. 1360 1220 # Allowing for changing group, but not coding web support just yet. 1361 1221 ($code,$msg) = updateUser($dbh, $webvar{uid}, $webvar{uname}, $webvar{gid}, $webvar{pass1}, … … 1364 1224 if ($code eq 'OK') { 1365 1225 $newperms{admin} = 1 if $webvar{accttype} eq 'S'; 1366 ($code,$msg) = changePermissions($dbh, 'user', $webvar{uid}, \%newperms, ($permstring eq 'i')); 1367 logaction(0, $session->param("username"), $curgroup, 1368 "Updated uid $webvar{uid}, user $webvar{uname} ($webvar{fname} $webvar{lname})"); 1226 ($code2,$msg2) = changePermissions($dbh, 'user', $webvar{uid}, \%newperms, ($permstring eq 'i')); 1369 1227 } 1370 1228 } 1371 1229 } 1372 1230 1373 if ($code eq 'OK' ) {1374 1231 if ($code eq 'OK' && $code2 eq 'OK') { 1232 my %pageparams = (page => "useradmin"); 1375 1233 if ($alterperms) { 1376 changepage(page => "useradmin", warnmsg =>1377 "You can only grant permissions you hold. $webvar{uname} ".1378 ($webvar{useraction} eq 'add' ? 'added' : 'updated')." with reduced access.");1234 $pageparams{warnmsg} = "You can only grant permissions you hold.\nUser ". 1235 ($webvar{useraction} eq 'add' ? "$webvar{uname} added" : "info updated for $webvar{uname}"). 1236 ".\nPermissions ".($webvar{useraction} eq 'add' ? 'added' : 'updated')." with reduced access."; 1379 1237 } else { 1380 changepage(page => "useradmin", resultmsg => "Successfully ". 1381 ($webvar{useraction} eq 'add' ? 'added' : 'updated')." user $webvar{uname}"); 1238 $pageparams{resultmsg} = "$msg".($webvar{useraction} eq 'add' ? '' : "\n$msg2"); 1382 1239 } 1240 changepage(%pageparams); 1383 1241 1384 1242 # add/update failed: … … 1399 1257 $page->param(pass1 => $webvar{pass1}); 1400 1258 $page->param(pass2 => $webvar{pass2}); 1401 $page->param(errmsg => $msg); 1259 $page->param(errmsg => "User info updated but permissions update failed: $msg2") if $code eq 'OK'; 1260 $page->param(errmsg => $msg) if $code ne 'OK'; 1402 1261 fill_permissions($page, \%newperms); 1403 1262 fill_actypelist($webvar{accttype}); 1404 1263 fill_clonemelist(); 1405 logaction(0, $session->param("username"), $curgroup, "Failed to $webvar{useraction} user ".1406 "$webvar{uname}: $msg")1407 if $config{log_failures};1408 1264 } 1409 1265 … … 1411 1267 1412 1268 changepage(page => "useradmin", errmsg => "You do not have permission to edit users") 1413 unless $permissions{admin} || $permissions{user_edit}; 1269 unless $permissions{admin} || $permissions{user_edit} || 1270 ($permissions{self_edit} && $session->param('uid') == $webvar{user}); 1414 1271 1415 1272 # security check - does the user have permission to access this entity? … … 1426 1283 fill_actypelist($userinfo->{type}); 1427 1284 # not using this yet, but adding it now means we can *much* more easily do so later. 1428 $page->param(gid => $ webvar{group_id});1285 $page->param(gid => $userinfo->{group_id}); 1429 1286 1430 1287 my %curperms; … … 1467 1324 $page->param(user => userFullName($dbh,$webvar{id})); 1468 1325 } elsif ($webvar{del} eq 'ok') { 1469 ##fixme: find group id user is in (for logging) *before* we delete the user1470 ##fixme: get other user data too for log1471 my $userref = getUserData($dbh, $webvar{id});1472 1326 my ($code,$msg) = delUser($dbh, $webvar{id}); 1473 1327 if ($code eq 'OK') { 1474 1328 # success. go back to the user list, do not pass "GO" 1475 # actions on users have a domain id of 0, always 1476 logaction(0, $session->param("username"), $curgroup, "Deleted user $webvar{id}/".$userref->{username}. 1477 " (".$userref->{lastname}.", ".$userref->{firstname}.")"); 1478 changepage(page => "useradmin", resultmsg => "Deleted user ".$userref->{username}. 1479 " (".$userref->{lastname}.", ".$userref->{firstname}.")"); 1329 changepage(page => "useradmin", resultmsg => $msg); 1480 1330 } else { 1481 # need to find failure mode 1482 $page->param(del_failed => 1); 1483 $page->param(errmsg => $msg); 1484 list_users($curgroup); 1485 logaction(0, $session->param("username"), $curgroup, "Failed to delete user ". 1486 "$webvar{id}/".$userref->{username}.": $msg") 1487 if $config{log_failures}; 1331 changepage(page => "useradmin", errmsg => $msg); 1488 1332 } 1489 1333 } else { 1490 1334 # cancelled. whee! 1491 1335 changepage(page => "useradmin"); 1336 } 1337 1338 } elsif ($webvar{page} eq 'loclist') { 1339 1340 changepage(page => "domlist", errmsg => "You are not allowed access to this function") 1341 unless $permissions{admin} || $permissions{location_view}; 1342 1343 # security check - does the user have permission to access this entity? 1344 # if (!check_scope(id => $webvar{id}, type => 'loc')) { 1345 # changepage(page => "loclist", errmsg => "You are not permitted to <foo> the requested location/view"); 1346 # } 1347 list_locations(); 1348 show_msgs(); 1349 1350 # Permissions! 1351 $page->param(addloc => $permissions{admin} || $permissions{location_create}); 1352 $page->param(delloc => $permissions{admin} || $permissions{location_delete}); 1353 1354 } elsif ($webvar{page} eq 'location') { 1355 1356 changepage(page => "domlist", errmsg => "You are not allowed access to this function") 1357 unless $permissions{admin} || $permissions{location_view}; 1358 1359 # security check - does the user have permission to access this entity? 1360 # if (!check_scope(id => $webvar{id}, type => 'loc')) { 1361 # changepage(page => "loclist", errmsg => "You are not permitted to <foo> the requested location/view"); 1362 # } 1363 1364 if ($webvar{locact} eq 'new') { 1365 # uuhhmm.... 1366 } elsif ($webvar{locact} eq 'add') { 1367 changepage(page => "loclist", errmsg => "You are not permitted to add locations/views", id => $webvar{parentid}) 1368 unless ($permissions{admin} || $permissions{location_create}); 1369 1370 my ($code,$msg) = addLoc($dbh, $curgroup, $webvar{locname}, $webvar{comments}, $webvar{iplist}); 1371 1372 if ($code eq 'OK' || $code eq 'WARN') { 1373 my %pageparams = (page => "loclist", id => $webvar{parentid}, 1374 defrec => $webvar{defrec}, revrec => $webvar{revrec}); 1375 $pageparams{warnmsg} = $msg."<br><br>\n".$DNSDB::resultstr if $code eq 'WARN'; 1376 $pageparams{resultmsg} = $DNSDB::resultstr if $code eq 'OK'; 1377 changepage(%pageparams); 1378 } else { 1379 $page->param(failed => 1); 1380 $page->param(errmsg => $msg); 1381 $page->param(wastrying => "adding"); 1382 $page->param(todo => "Add location/view"); 1383 $page->param(locact => "add"); 1384 $page->param(id => $webvar{id}); 1385 $page->param(locname => $webvar{locname}); 1386 $page->param(comments => $webvar{comments}); 1387 $page->param(iplist => $webvar{iplist}); 1388 } 1389 1390 } elsif ($webvar{locact} eq 'edit') { 1391 changepage(page => "loclist", errmsg => "You are not permitted to edit locations/views", id => $webvar{parentid}) 1392 unless ($permissions{admin} || $permissions{location_edit}); 1393 1394 my $loc = getLoc($dbh, $webvar{loc}); 1395 $page->param(wastrying => "editing"); 1396 $page->param(todo => "Edit location/view"); 1397 $page->param(locact => "update"); 1398 $page->param(id => $webvar{loc}); 1399 $page->param(locname => $loc->{description}); 1400 $page->param(comments => $loc->{comments}); 1401 $page->param(iplist => $loc->{iplist}); 1402 1403 } elsif ($webvar{locact} eq 'update') { 1404 changepage(page => "loclist", errmsg => "You are not permitted to edit locations/views", id => $webvar{parentid}) 1405 unless ($permissions{admin} || $permissions{location_edit}); 1406 1407 my ($code,$msg) = updateLoc($dbh, $webvar{id}, $curgroup, $webvar{locname}, $webvar{comments}, $webvar{iplist}); 1408 1409 if ($code eq 'OK') { 1410 changepage(page => "loclist", resultmsg => $msg); 1411 } else { 1412 $page->param(failed => 1); 1413 $page->param(errmsg => $msg); 1414 $page->param(wastrying => "editing"); 1415 $page->param(todo => "Edit location/view"); 1416 $page->param(locact => "update"); 1417 $page->param(id => $webvar{loc}); 1418 $page->param(locname => $webvar{locname}); 1419 $page->param(comments => $webvar{comments}); 1420 $page->param(iplist => $webvar{iplist}); 1421 } 1422 } else { 1423 changepage(page => "loclist", errmsg => "You are not permitted to add locations/views", id => $webvar{parentid}) 1424 unless ($permissions{admin} || $permissions{location_create}); 1425 1426 $page->param(todo => "Add location/view"); 1427 $page->param(locact => "add"); 1428 $page->param(locname => ($webvar{locname} ? $webvar{locname} : '')); 1429 $page->param(iplist => ($webvar{iplist} ? $webvar{iplist} : '')); 1430 1431 show_msgs(); 1492 1432 } 1493 1433 … … 1571 1511 $page->param(forcettl => $webvar{forcettl}) if $webvar{forcettl}; 1572 1512 $page->param(newttl => $webvar{newttl}) if $webvar{newttl}; 1513 # This next one is arguably better on by default, but Breaking Things Is Bad, Mmmkay? 1514 $page->param(mergematching => $webvar{mergematching}) if $webvar{mergematching}; 1573 1515 $page->param(dominactive => 1) if (!$webvar{domactive} && $webvar{doit}); # eww. 1574 1516 $page->param(importdoms => $webvar{importdoms}) if $webvar{importdoms}; … … 1589 1531 } 1590 1532 1533 # Bizarre Things Happen when you AXFR a null-named zone. 1534 $webvar{importdoms} =~ s/^\s+//; 1591 1535 my @domlist = split /\s+/, $webvar{importdoms}; 1592 1536 my @results; … … 1594 1538 my %row; 1595 1539 my ($code,$msg) = importAXFR($dbh, $webvar{ifrom}, $domain, $webvar{group}, 1596 $webvar{domstatus}, $webvar{rwsoa}, $webvar{rwns}, ($webvar{forcettl} ? $webvar{newttl} : 0) ); 1540 $webvar{domstatus}, $webvar{rwsoa}, $webvar{rwns}, ($webvar{forcettl} ? $webvar{newttl} : 0), 1541 $webvar{mergematching}); 1597 1542 $row{domok} = $msg if $code eq 'OK'; 1598 1543 if ($code eq 'WARN') { … … 1605 1550 } 1606 1551 $msg = "<br />\n".$msg if $msg =~ m|<br />|; 1607 logaction(domainID($dbh, $domain), $session->param("username"), $webvar{group},1608 "AXFR import $domain from $webvar{ifrom} ($code): $msg");1609 1552 $row{domain} = $domain; 1610 1553 push @results, \%row; … … 1649 1592 } elsif ($webvar{page} eq 'log') { 1650 1593 1651 ##fixme put in some real log-munching stuff1652 my $sql = "SELECT user_id, email, name, entry, date_trunc('second',stamp) FROM log WHERE ";1653 1594 my $id = $curgroup; # we do this because the group log may be called from (almost) any page, 1654 1595 # but the others are much more limited. this is probably non-optimal. 1655 1596 1656 1597 if ($webvar{ltype} && $webvar{ltype} eq 'user') { 1657 $sql .= "user_id=?"; 1598 ##fixme: where should we call this from? 1658 1599 $id = $webvar{id}; 1659 1600 if (!check_scope(id => $id, type => 'user')) { … … 1663 1604 $page->param(logfor => 'user '.userFullName($dbh,$id)); 1664 1605 } elsif ($webvar{ltype} && $webvar{ltype} eq 'dom') { 1665 $sql .= "domain_id=?";1666 1606 $id = $webvar{id}; 1667 1607 if (!check_scope(id => $id, type => 'domain')) { … … 1671 1611 $page->param(logfor => 'domain '.domainName($dbh,$id)); 1672 1612 } elsif ($webvar{ltype} && $webvar{ltype} eq 'rdns') { 1673 $sql .= "rdns_id=?";1674 1613 $id = $webvar{id}; 1675 1614 if (!check_scope(id => $id, type => 'revzone')) { … … 1680 1619 } else { 1681 1620 # Default to listing curgroup log 1682 $sql .= "group_id=?";1683 1621 $page->param(logfor => 'group '.groupName($dbh,$id)); 1684 1622 # note that scope limitations are applied via the change-group check; 1685 1623 # group log is always for the "current" group 1686 1624 } 1625 $webvar{ltype} = 'group' if !$webvar{ltype}; 1626 my $lcount = getLogCount($dbh, (id => $id, logtype => $webvar{ltype})) or push @debugbits, $DNSDB::errstr; 1627 1628 $page->param(id => $id); 1629 $page->param(ltype => $webvar{ltype}); 1630 1631 fill_fpnla($lcount); 1632 fill_pgcount($lcount, "log entries", ''); 1633 $page->param(curpage => $webvar{page}.($webvar{ltype} ? "&ltype=$webvar{ltype}" : '')); 1634 1635 $sortby = 'stamp'; 1636 $sortorder = 'DESC'; # newest-first; although filtering is probably going to be more useful than sorting 1637 # sort/order 1638 $session->param($webvar{page}.'sortby', $webvar{sortby}) if $webvar{sortby}; 1639 $session->param($webvar{page}.'order', $webvar{order}) if $webvar{order}; 1640 1641 $sortby = $session->param($webvar{page}.'sortby') if $session->param($webvar{page}.'sortby'); 1642 $sortorder = $session->param($webvar{page}.'order') if $session->param($webvar{page}.'order'); 1643 1644 # Set up the column headings with the sort info 1645 my @cols = ('fname','username','entry','stamp'); 1646 my %colnames = (fname => 'Name', username => 'Username/Email', entry => 'Log Entry', stamp => 'Date/Time'); 1647 fill_colheads($sortby, $sortorder, \@cols, \%colnames); 1648 1649 ##fixme: increase per-page limit or use separate limit for log? some ops give *lots* of entries... 1650 my $logentries = getLogEntries($dbh, (id => $id, logtype => $webvar{ltype}, 1651 offset => $webvar{offset}, sortby => $sortby, sortorder => $sortorder)); 1652 $page->param(logentries => $logentries); 1653 1687 1654 ##fixme: 1688 1655 # - filtering 1689 1656 # - show reverse zone column? 1690 # - pagination/limiting number of records - put newest-first so user 1691 # doesn't always need to go to the last page for recent activity? 1692 my $sth = $dbh->prepare($sql); 1693 $sth->execute($id); 1694 my @logbits; 1695 while (my ($uid, $email, $name, $entry, $stamp) = $sth->fetchrow_array) { 1696 my %row; 1697 $row{userfname} = $name; 1698 $row{userid} = $uid; 1699 $row{useremail} = $email; 1700 $row{logentry} = $entry; 1701 ($row{logtime}) = ($stamp =~ /^(.+)-\d\d$/); 1702 push @logbits, \%row; 1703 } 1704 $page->param(logentries => \@logbits); 1657 # - on log record creation, bundle "parented" log actions (eg, "AXFR record blah for domain foo", 1658 # or "Add record bar for new domain baz") into one entry (eg, "AXFR domain foo", "Add domain baz")? 1659 # need a way to expand this into the complete list, and to exclude "child" entries 1705 1660 1706 1661 # scope check fail target … … 1714 1669 1715 1670 ##common bits 1671 # mostly things in the menu 1716 1672 if ($webvar{page} ne 'login' && $webvar{page} ne 'badpage') { 1717 1673 $page->param(username => $session->param("username")); … … 1724 1680 ##fixme 1725 1681 $page->param(mayrdns => 1); 1682 1683 $page->param(mayloc => ($permissions{admin} || $permissions{location_view})); 1726 1684 1727 1685 $page->param(maydefrec => $permissions{admin}); … … 1811 1769 # than set them locally everywhere. 1812 1770 foreach my $sessme ('resultmsg','warnmsg','errmsg') { 1813 if ($params{$sessme}) { 1814 $session->param($sessme, $params{$sessme}); 1771 if (my $tmp = $params{$sessme}) { 1772 $tmp =~ s/^\n//; 1773 $tmp =~ s|\n|<br />\n|g; 1774 $session->param($sessme, $tmp); 1815 1775 delete $params{$sessme}; 1816 1776 } … … 1830 1790 } # end changepage 1831 1791 1792 # wrap up the usual suspects for result, warning, or error messages to be displayed 1793 sub show_msgs { 1794 if ($session->param('resultmsg')) { 1795 $page->param(resultmsg => $session->param('resultmsg')); 1796 $session->clear('resultmsg'); 1797 } 1798 if ($session->param('warnmsg')) { 1799 $page->param(warnmsg => $session->param('warnmsg')); 1800 $session->clear('warnmsg'); 1801 } 1802 if ($session->param('errmsg')) { 1803 $page->param(errmsg => $session->param('errmsg')); 1804 $session->clear('errmsg'); 1805 } 1806 } # end show_msgs 1807 1832 1808 sub fillsoa { 1833 my $def = shift; 1809 my $defrec = shift; 1810 my $revrec = shift; 1834 1811 my $id = shift; 1835 my $domname = ($def eq 'y' ? '' : "DOMAIN"); 1836 1837 $page->param(defrec => $def); 1812 my $preserve = shift || 'd'; # Flag to use webvar fields or retrieve from database 1813 1814 my $domname = ($defrec eq 'y' ? '' : "DOMAIN"); 1815 1816 $page->param(defrec => $defrec); 1817 $page->param(revrec => $revrec); 1838 1818 1839 1819 # i had a good reason to do this when I wrote it... 1840 1820 # $page->param(domain => $domname); 1841 1821 # $page->param(group => $DNSDB::group); 1842 $page->param(isgrp => 1) if $def eq 'y'; 1843 $page->param(parent => ($def eq 'y' ? groupName($dbh, $DNSDB::group) : domainName($dbh, $id)) ); 1822 $page->param(isgrp => 1) if $defrec eq 'y'; 1823 $page->param(parent => ($defrec eq 'y' ? groupName($dbh, $id) : 1824 ($revrec eq 'n' ? domainName($dbh, $id) : revName($dbh, $id)) ) ); 1844 1825 1845 1826 # defaults … … 1852 1833 $page->param(defminttl => $DNSDB::def{minttl}); 1853 1834 1854 # there are probably better ways to do this. TMTOWTDI.1855 my %soa = getSOA($dbh,$def,$id);1856 1857 1835 $page->param(id => $id); 1858 $page->param(recid => $soa{recid}); 1859 $page->param(prins => ($soa{prins} ? $soa{prins} : $DNSDB::def{prins})); 1860 $page->param(contact => ($soa{contact} ? $soa{contact} : $DNSDB::def{contact})); 1861 $page->param(refresh => ($soa{refresh} ? $soa{refresh} : $DNSDB::def{refresh})); 1862 $page->param(retry => ($soa{retry} ? $soa{retry} : $DNSDB::def{retry})); 1863 $page->param(expire => ($soa{expire} ? $soa{expire} : $DNSDB::def{expire})); 1864 $page->param(minttl => ($soa{minttl} ? $soa{minttl} : $DNSDB::def{minttl})); 1865 $page->param(ttl => ($soa{ttl} ? $soa{ttl} : $DNSDB::def{soattl})); 1836 1837 if ($preserve eq 'd') { 1838 # there are probably better ways to do this. TMTOWTDI. 1839 my $soa = getSOA($dbh,$defrec,$revrec,$id); 1840 1841 $page->param(prins => ($soa->{prins} ? $soa->{prins} : $DNSDB::def{prins})); 1842 $page->param(contact => ($soa->{contact} ? $soa->{contact} : $DNSDB::def{contact})); 1843 $page->param(refresh => ($soa->{refresh} ? $soa->{refresh} : $DNSDB::def{refresh})); 1844 $page->param(retry => ($soa->{retry} ? $soa->{retry} : $DNSDB::def{retry})); 1845 $page->param(expire => ($soa->{expire} ? $soa->{expire} : $DNSDB::def{expire})); 1846 $page->param(minttl => ($soa->{minttl} ? $soa->{minttl} : $DNSDB::def{minttl})); 1847 $page->param(ttl => ($soa->{ttl} ? $soa->{ttl} : $DNSDB::def{soattl})); 1848 } else { 1849 $page->param(prins => ($webvar{prins} ? $webvar{prins} : $DNSDB::def{prins})); 1850 $page->param(contact => ($webvar{contact} ? $webvar{contact} : $DNSDB::def{contact})); 1851 $page->param(refresh => ($webvar{refresh} ? $webvar{refresh} : $DNSDB::def{refresh})); 1852 $page->param(retry => ($webvar{retry} ? $webvar{retry} : $DNSDB::def{retry})); 1853 $page->param(expire => ($webvar{expire} ? $webvar{expire} : $DNSDB::def{expire})); 1854 $page->param(minttl => ($webvar{minttl} ? $webvar{minttl} : $DNSDB::def{minttl})); 1855 $page->param(ttl => ($webvar{ttl} ? $webvar{ttl} : $DNSDB::def{soattl})); 1856 } 1866 1857 } 1867 1858 … … 1872 1863 1873 1864 # get the SOA first 1874 my %soa = getSOA($dbh,$def,$rev,$id);1875 1876 $page->param(contact => $soa {contact});1877 $page->param(prins => $soa {prins});1878 $page->param(refresh => $soa {refresh});1879 $page->param(retry => $soa {retry});1880 $page->param(expire => $soa {expire});1881 $page->param(minttl => $soa {minttl});1882 $page->param(ttl => $soa {ttl});1883 1884 my $foo2 = getDomRecs($dbh, $def,$rev,$id,$perpage,$webvar{offset},$sortby,$sortorder,$filter);1885 1886 my $row = 0; 1865 my $soa = getSOA($dbh,$def,$rev,$id); 1866 1867 $page->param(contact => $soa->{contact}); 1868 $page->param(prins => $soa->{prins}); 1869 $page->param(refresh => $soa->{refresh}); 1870 $page->param(retry => $soa->{retry}); 1871 $page->param(expire => $soa->{expire}); 1872 $page->param(minttl => $soa->{minttl}); 1873 $page->param(ttl => $soa->{ttl}); 1874 1875 my $foo2 = getDomRecs($dbh,(defrec => $def, revrec => $rev, id => $id, offset => $webvar{offset}, 1876 sortby => $sortby, sortorder => $sortorder, filter => $filter)); 1877 1887 1878 foreach my $rec (@$foo2) { 1888 1879 $rec->{type} = $typemap{$rec->{type}}; 1889 $rec->{row} = $row % 2;1890 $rec->{defrec} = $def;1891 $rec->{revrec} = $rev;1892 1880 $rec->{sid} = $webvar{sid}; 1893 $rec->{id} = $id;1894 1881 $rec->{fwdzone} = $rev eq 'n'; 1895 1882 $rec->{distance} = 'n/a' unless ($rec->{type} eq 'MX' || $rec->{type} eq 'SRV'); 1896 1883 $rec->{weight} = 'n/a' unless ($rec->{type} eq 'SRV'); 1897 1884 $rec->{port} = 'n/a' unless ($rec->{type} eq 'SRV'); 1898 $row++;1899 1885 # ACLs 1900 1886 $rec->{record_edit} = ($permissions{admin} || $permissions{record_edit}); 1901 1887 $rec->{record_delete} = ($permissions{admin} || $permissions{record_delete}); 1888 $rec->{locname} = '' unless ($permissions{admin} || $permissions{location_view}); 1902 1889 } 1903 1890 $page->param(reclist => $foo2); … … 1914 1901 if ($webvar{revrec} eq 'n') { 1915 1902 my $domroot = ($webvar{defrec} eq 'y' ? 'DOMAIN' : domainName($dbh,$webvar{parentid})); 1916 $page->param(name => $domroot);1903 $page->param(name => ($webvar{name} ? $webvar{name} : $domroot)); 1917 1904 $page->param(address => $webvar{address}); 1918 1905 $page->param(distance => $webvar{distance}) … … 1928 1915 } 1929 1916 # retrieve the right ttl instead of falling (way) back to the hardcoded system default 1930 my %soa = getSOA($dbh,$webvar{defrec},$webvar{revrec},$webvar{parentid});1931 $page->param(ttl => ($webvar{ttl} ? $webvar{ttl} : $soa {minttl}));1917 my $soa = getSOA($dbh,$webvar{defrec},$webvar{revrec},$webvar{parentid}); 1918 $page->param(ttl => ($webvar{ttl} ? $webvar{ttl} : $soa->{minttl})); 1932 1919 } 1933 1920 … … 1949 1936 1950 1937 sub fill_clonemelist { 1951 my $sth = $dbh->prepare("SELECT username,user_id FROM users WHERE group_id=$curgroup");1952 $sth->execute;1953 1954 1938 # shut up some warnings, but don't stomp on caller's state 1955 1939 local $webvar{clonesrc} = 0 if !defined($webvar{clonesrc}); 1956 1940 1957 my @clonesrc; 1958 while (my ($username,$uid) = $sth->fetchrow_array) { 1959 my %row = ( 1960 username => $username, 1961 uid => $uid, 1962 selected => ($webvar{clonesrc} == $uid ? 1 : 0) 1963 ); 1964 push @clonesrc, \%row; 1965 } 1966 $page->param(clonesrc => \@clonesrc); 1941 my $clones = getUserDropdown($dbh, $curgroup, $webvar{clonesrc}); 1942 $page->param(clonesrc => $clones); 1967 1943 } 1968 1944 … … 2091 2067 my $childlist = join(',',@childgroups); 2092 2068 2093 my $sql = "SELECT count(*) FROM groups WHERE parent_group_id IN ($curgroup".($childlist ? ",$childlist" : '').")". 2094 ($startwith ? " AND group_name ~* ?" : ''). 2095 ($filter ? " AND group_name ~* ?" : ''); 2096 my $sth = $dbh->prepare($sql); 2097 $sth->execute(@filterargs); 2098 my ($count) = ($sth->fetchrow_array); 2069 my ($count) = getGroupCount($dbh, (childlist => $childlist, curgroup => $curgroup, 2070 filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef) ) ); 2099 2071 2100 2072 # fill page count and first-previous-next-last-all bits … … 2113 2085 2114 2086 # set up the headers 2115 my @cols = ('group','parent','nusers','ndomains' );2116 my %colnames = (group => 'Group', parent => 'Parent Group', nusers => 'Users', ndomains => 'Domains' );2087 my @cols = ('group','parent','nusers','ndomains','nrevzones'); 2088 my %colnames = (group => 'Group', parent => 'Parent Group', nusers => 'Users', ndomains => 'Domains', nrevzones => 'Reverse Zones'); 2117 2089 fill_colheads($sortby, $sortorder, \@cols, \%colnames); 2118 2090 … … 2127 2099 $sortby = 'g2.group_name' if $sortby eq 'parent'; 2128 2100 2129 my @grouplist; 2130 $sql = "SELECT g.group_id, g.group_name, g2.group_name, ". 2131 "count(distinct(u.username)) AS nusers, count(distinct(d.domain)) AS ndomains ". 2132 "FROM groups g ". 2133 "INNER JOIN groups g2 ON g2.group_id=g.parent_group_id ". 2134 "LEFT OUTER JOIN users u ON u.group_id=g.group_id ". 2135 "LEFT OUTER JOIN domains d ON d.group_id=g.group_id ". 2136 "WHERE g.parent_group_id IN ($curgroup".($childlist ? ",$childlist" : '').") ". 2137 ($startwith ? " AND g.group_name ~* ?" : ''). 2138 ($filter ? " AND g.group_name ~* ?" : ''). 2139 " GROUP BY g.group_id, g.group_name, g2.group_name ". 2140 " ORDER BY $sortby $sortorder ". 2141 ($offset eq 'all' ? '' : " LIMIT $perpage OFFSET ".$offset*$perpage); 2142 $sth = $dbh->prepare($sql); 2143 $sth->execute(@filterargs); 2144 2145 my $rownum = 0; 2146 while (my @data = $sth->fetchrow_array) { 2147 my %row; 2148 $row{groupid} = $data[0]; 2149 $row{groupname} = $data[1]; 2150 $row{pgroup} = $data[2]; 2151 $row{nusers} = $data[3]; 2152 $row{ndomains} = $data[4]; 2153 $row{bg} = ($rownum++)%2; 2154 $row{sid} = $sid; 2155 $row{edgrp} = ($permissions{admin} || $permissions{group_edit}); 2156 $row{delgrp} = ($permissions{admin} || $permissions{group_delete}); 2157 push @grouplist, \%row; 2158 } 2159 $page->param(grouptable => \@grouplist); 2101 my $glist = getGroupList($dbh, (childlist => $childlist, curgroup => $curgroup, 2102 filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef), 2103 offset => $webvar{offset}, sortby => $sortby, sortorder => $sortorder) ); 2104 2105 $page->param(grouptable => $glist); 2160 2106 } # end listgroups() 2161 2107 … … 2165 2111 my $cur = shift || $curgroup; 2166 2112 2167 my @childgroups; 2168 getChildren($dbh, $logingroup, \@childgroups, 'all'); 2169 my $childlist = join(',',@childgroups); 2170 2171 ##fixme: need to reorder list so that we can display a pseudotree in group dropdowns 2172 2173 # weesa gonna discard parent_group_id for now 2174 my $sth = $dbh->prepare("SELECT group_id,parent_group_id,group_name FROM groups ". 2175 "WHERE group_id IN ($logingroup".($childlist ? ",$childlist" : '').")". 2176 "ORDER BY group_id"); 2177 $sth->execute; 2113 # little recursive utility sub-sub 2114 sub getgroupdrop { 2115 my $root = shift; 2116 my $cur = shift; # to tag the selected group 2117 my $grplist = shift; 2118 my $indent = shift || ' '; 2119 2120 my @childlist; 2121 getChildren($dbh,$root,\@childlist,'immediate'); 2122 return if $#childlist == -1; 2123 foreach (@childlist) { 2124 my %row; 2125 $row{groupval} = $_; 2126 $row{groupactive} = ($_ == $cur); 2127 $row{groupname} = $indent.groupName($dbh, $_); 2128 push @{$grplist}, \%row; 2129 getgroupdrop($_, $cur, $grplist, $indent.' '); 2130 } 2131 } 2132 2178 2133 my @grouplist; 2179 while (my ($groupid,$pargroup,$groupname) = $sth->fetchrow_array()) { 2180 my %row; 2181 $row{groupname} = $groupname; 2182 $row{groupval} = $groupid; 2183 ##fixme: need magic 2184 ## ... WTF? 2185 # $row{defgroup} = ''; 2186 $row{groupactive} = 1 if $groupid == $cur; 2187 push @grouplist, \%row; 2188 } 2134 push @grouplist, { groupval => $logingroup, groupactive => $logingroup == $curgroup, 2135 groupname => groupName($dbh, $logingroup) }; 2136 getgroupdrop($logingroup, $curgroup, \@grouplist); 2189 2137 2190 2138 $page->param("$template_var" => \@grouplist); 2191 2192 2139 } # end fill_grouplist() 2140 2141 2142 sub fill_loclist { 2143 my $cur = shift || $curgroup; 2144 my $defloc = shift || ''; 2145 2146 return unless ($permissions{admin} || $permissions{location_view}); 2147 2148 $page->param(location_view => ($permissions{admin} || $permissions{location_view})); 2149 2150 if ($permissions{admin} || $permissions{record_locchg}) { 2151 my $loclist = getLocDropdown($dbh, $cur, $defloc); 2152 $page->param(record_locchg => 1); 2153 $page->param(loclist => $loclist); 2154 } else { 2155 my $loc = getLoc($dbh, $defloc); 2156 $page->param(loc_name => $loc->{description}); 2157 } 2158 } # end fill_loclist() 2193 2159 2194 2160 … … 2199 2165 my $childlist = join(',',@childgroups); 2200 2166 2201 my $sql = "SELECT count(*) FROM users WHERE group_id IN ($curgroup".($childlist ? ",$childlist" : '').")". 2202 ($startwith ? " AND username ~* ?" : ''). 2203 ($filter ? " AND username ~* ?" : ''); 2204 my $sth = $dbh->prepare($sql); 2205 $sth->execute(@filterargs); 2206 my ($count) = ($sth->fetchrow_array); 2167 my $count = getUserCount($dbh, (childlist => $childlist, curgroup => $curgroup, 2168 filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef) ) ); 2207 2169 2208 2170 # fill page count and first-previous-next-last-all bits … … 2229 2191 $page->param(searchsubs => $searchsubs) if $searchsubs; 2230 2192 2231 # munge sortby for columns in database 2232 $sortby = 'u.username' if $sortby eq 'user'; 2233 $sortby = 'u.type' if $sortby eq 'type'; 2234 $sortby = 'g.group_name' if $sortby eq 'group'; 2235 $sortby = 'u.status' if $sortby eq 'status'; 2236 2237 my @userlist; 2238 $sql = "SELECT u.user_id, u.username, u.firstname || ' ' || u.lastname AS fname, u.type, g.group_name, u.status ". 2239 "FROM users u ". 2240 "INNER JOIN groups g ON u.group_id=g.group_id ". 2241 "WHERE u.group_id IN ($curgroup".($childlist ? ",$childlist" : '').")". 2242 ($startwith ? " AND u.username ~* ?" : ''). 2243 ($filter ? " AND u.username ~* ?" : ''). 2244 " ORDER BY $sortby $sortorder ". 2245 ($offset eq 'all' ? '' : " LIMIT $perpage OFFSET ".$offset*$perpage); 2246 2247 $sth = $dbh->prepare($sql); 2248 $sth->execute(@filterargs); 2249 2250 my $rownum = 0; 2251 while (my @data = $sth->fetchrow_array) { 2252 no warnings "uninitialized"; # Just In Case something stupid happens and a user gets no first or last name 2253 my %row; 2254 $row{userid} = $data[0]; 2255 $row{username} = $data[1]; 2256 $row{userfull} = $data[2]; 2257 $row{usertype} = ($data[3] eq 'S' ? 'superuser' : "user"); 2258 $row{usergroup} = $data[4]; 2259 $row{active} = $data[5]; 2260 $row{bg} = ($rownum++)%2; 2261 $row{sid} = $sid; 2262 $row{eduser} = ($permissions{admin} || $permissions{user_edit}); 2263 $row{deluser} = ($permissions{admin} || $permissions{user_delete}); 2264 push @userlist, \%row; 2265 } 2266 $page->param(usertable => \@userlist); 2193 my $ulist = getUserList($dbh, (childlist => $childlist, curgroup => $curgroup, 2194 filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef), 2195 offset => $webvar{offset}, sortby => $sortby, sortorder => $sortorder) ); 2196 # Some UI things need to be done to the list (unlike other lists) 2197 foreach my $u (@{$ulist}) { 2198 $u->{eduser} = ($permissions{admin} || 2199 ($permissions{user_edit} && $u->{type} ne 'S') || 2200 ($permissions{self_edit} && $u->{user_id} == $session->param('uid')) ); 2201 $u->{deluser} = ($permissions{admin} || ($permissions{user_delete} && $u->{type} ne 'S')); 2202 $u->{type} = ($u->{type} eq 'S' ? 'superuser' : 'user'); 2203 } 2204 $page->param(usertable => $ulist); 2267 2205 } # end list_users() 2206 2207 2208 sub list_locations { 2209 2210 my @childgroups; 2211 getChildren($dbh, $curgroup, \@childgroups, 'all') if $searchsubs; 2212 my $childlist = join(',',@childgroups); 2213 2214 my $count = getLocCount($dbh, (childlist => $childlist, curgroup => $curgroup, 2215 filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef) ) ); 2216 2217 # fill page count and first-previous-next-last-all bits 2218 fill_pgcount($count,"locations/views",''); 2219 fill_fpnla($count); 2220 2221 $sortby = 'user'; 2222 # sort/order 2223 $session->param($webvar{page}.'sortby', $webvar{sortby}) if $webvar{sortby}; 2224 $session->param($webvar{page}.'order', $webvar{order}) if $webvar{order}; 2225 2226 $sortby = $session->param($webvar{page}.'sortby') if $session->param($webvar{page}.'sortby'); 2227 $sortorder = $session->param($webvar{page}.'order') if $session->param($webvar{page}.'order'); 2228 2229 # set up the headers 2230 my @cols = ('description', 'iplist', 'group'); 2231 my %colnames = (description => 'Location/View Name', iplist => 'Permitted IPs/Ranges', group => 'Group'); 2232 fill_colheads($sortby, $sortorder, \@cols, \%colnames); 2233 2234 # waffle, waffle - keep state on these as well as sortby, sortorder? 2235 $page->param("start$startwith" => 1) if $startwith && $startwith =~ /^(?:[a-z]|0-9)$/; 2236 2237 $page->param(filter => $filter) if $filter; 2238 $page->param(searchsubs => $searchsubs) if $searchsubs; 2239 2240 my $loclist = getLocList($dbh, (childlist => $childlist, curgroup => $curgroup, 2241 filter => ($filter ? $filter : undef), startwith => ($startwith ? $startwith : undef), 2242 offset => $webvar{offset}, sortby => $sortby, sortorder => $sortorder) ); 2243 # Some UI things need to be done to the list 2244 foreach my $l (@{$loclist}) { 2245 $l->{iplist} = "(All IPs)" if !$l->{iplist}; 2246 $l->{edloc} = ($permissions{admin} || $permissions{loc_edit}); 2247 $l->{delloc} = ($permissions{admin} || $permissions{loc_delete}); 2248 } 2249 $page->param(loctable => $loclist); 2250 } # end list_locations() 2268 2251 2269 2252 … … 2282 2265 foreach my $col (@$cols) { 2283 2266 my %coldata; 2284 $coldata{firstcol} = 1 if $col eq $cols->[0];2285 2267 $coldata{sid} = $sid; 2286 2268 $coldata{page} = $webvar{page}; … … 2307 2289 2308 2290 2309 sub logaction {2310 my $domid = shift;2311 my $username = shift;2312 my $groupid = shift;2313 my $entry = shift;2314 my $revid = shift || 0;2315 2316 ##fixme: push SQL into DNSDB.pm2317 ##fixme: add bits to retrieve group/domain name info to retain after entity is deleted?2318 my $sth = $dbh->prepare("SELECT user_id, firstname || ' ' || lastname FROM users WHERE username=?");2319 $sth->execute($username);2320 my ($user_id, $fullname) = $sth->fetchrow_array;2321 2322 $sth = $dbh->prepare("INSERT INTO log (domain_id,user_id,group_id,email,name,entry,rdns_id) ".2323 "VALUES (?,?,?,?,?,?,?)") or warn $dbh->errstr;2324 $sth->execute($domid,$user_id,$groupid,$username,$fullname,$entry,$revid) or warn $sth->errstr;2325 } # end logaction()2326 2327 2328 2291 # we have to do this in a variety of places; let's make it consistent 2329 2292 sub fill_permissions { -
branches/stable/dns.sql
r544 r545 14 14 15 15 COPY misc (misc_id, key, value) FROM stdin; 16 1 dbversion 1.0 17 \. 16 1 dbversion 1.2 17 \. 18 19 CREATE TABLE locations ( 20 location character varying (4) PRIMARY KEY, 21 loc_id serial UNIQUE, 22 group_id integer NOT NULL DEFAULT 1, 23 iplist text NOT NULL DEFAULT '', 24 description character varying(40) NOT NULL DEFAULT '', 25 comments text NOT NULL DEFAULT '' 26 ); 18 27 19 28 CREATE TABLE default_records ( … … 54 63 1 1 hostmaster.ADMINDOMAIN:ns1.ADMINDOMAIN 6 3600:900:1048576:2560 3600 55 64 2 1 unused-%r.ADMINDOMAIN 65283 ZONE 3600 65 3 1 ns2.example.com 2 ZONE 7200 \N 66 4 1 ns1.example.com 2 ZONE 7200 \N 56 67 \. 57 68 58 69 CREATE TABLE domains ( 59 70 domain_id serial NOT NULL, 60 "domain" character varying(80) NOT NULL ,71 "domain" character varying(80) NOT NULL PRIMARY KEY, 61 72 group_id integer DEFAULT 1 NOT NULL, 62 73 description character varying(255) DEFAULT ''::character varying NOT NULL, 63 74 status integer DEFAULT 1 NOT NULL, 64 75 zserial integer, 65 sertype character(1) DEFAULT 'D'::bpchar 76 sertype character(1) DEFAULT 'D'::bpchar, 77 changed boolean DEFAULT true NOT NULL, 78 default_location character varying (4) DEFAULT '' NOT NULL 66 79 ); 67 80 68 81 CREATE TABLE revzones ( 69 82 rdns_id serial NOT NULL, 70 revnet cidr NOT NULL ,83 revnet cidr NOT NULL PRIMARY KEY, 71 84 group_id integer DEFAULT 1 NOT NULL, 72 85 description character varying(255) DEFAULT ''::character varying NOT NULL, 73 86 status integer DEFAULT 1 NOT NULL, 74 87 zserial integer, 75 sertype character(1) DEFAULT 'D'::bpchar 88 sertype character(1) DEFAULT 'D'::bpchar, 89 changed boolean DEFAULT true NOT NULL, 90 default_location character varying (4) DEFAULT '' NOT NULL 76 91 ); 77 92 … … 119 134 record_edit boolean DEFAULT false NOT NULL, 120 135 record_delete boolean DEFAULT false NOT NULL, 136 record_locchg boolean DEFAULT false NOT NULL, 137 location_create boolean DEFAULT false NOT NULL, 138 location_edit boolean DEFAULT false NOT NULL, 139 location_delete boolean DEFAULT false NOT NULL, 140 location_view boolean DEFAULT false NOT NULL, 121 141 user_id integer UNIQUE, 122 142 group_id integer UNIQUE … … 124 144 125 145 -- Need *two* basic permissions; one for the initial group, one for the default admin user 126 COPY permissions (permission_id, admin, self_edit, group_create, group_edit, group_delete, user_create, user_edit, user_delete, domain_create, domain_edit, domain_delete, record_create, record_edit, record_delete, user_id, group_id) FROM stdin;127 1 f f f f f f f f t t t t t t \N 1128 2 t f f f f f f f f f f f f f 1 \N146 COPY permissions (permission_id, "admin", self_edit, group_create, group_edit, group_delete, user_create, user_edit, user_delete, domain_create, domain_edit, domain_delete, record_create, record_edit, record_delete, record_locchg, location_create, location_edit, location_delete, location_view, user_id, group_id) FROM stdin; 147 1 f f f f f f f f t t t t t t f f f f f \N 1 148 2 t f f f f f f f f f f f f f f f f f f 1 \N 129 149 \. 130 150 … … 141 161 port integer DEFAULT 0 NOT NULL, 142 162 ttl integer DEFAULT 7200 NOT NULL, 143 description text 163 description text, 164 location character varying (4) DEFAULT '' NOT NULL 144 165 ); 145 166 … … 156 177 COPY rectypes (val, name, stdflag, listorder, alphaorder) FROM stdin; 157 178 1 A 1 1 1 158 2 NS 1 537179 2 NS 2 9 37 159 180 3 MD 5 255 29 160 181 4 MF 5 255 30 161 5 CNAME 1 79182 5 CNAME 2 11 9 162 183 6 SOA 0 0 53 163 184 7 MB 5 255 28 … … 166 187 10 NULL 5 255 43 167 188 11 WKS 5 255 64 168 12 PTR 3 1046189 12 PTR 3 5 46 169 190 13 HINFO 5 255 18 170 191 14 MINFO 5 255 32 171 15 MX 1 634172 16 TXT 1 860192 15 MX 1 10 34 193 16 TXT 2 12 60 173 194 17 RP 4 255 48 174 195 18 AFSDB 5 255 4 … … 187 208 31 EID 5 255 15 188 209 32 NIMLOC 5 255 36 189 33 SRV 1 955210 33 SRV 1 13 55 190 211 34 ATMA 5 255 6 191 212 35 NAPTR 5 255 35 … … 226 247 65280 A+PTR 2 2 2 227 248 65281 AAAA+PTR 2 4 4 228 65282 PTR template 3 11 2 229 65283 A+PTR template 3 12 2 230 65284 AAAA+PTR template 3 13 2 249 65282 PTR template 3 6 2 250 65283 A+PTR template 2 7 2 251 65284 AAAA+PTR template 8 13 2 252 65285 Delegation 2 8 2 231 253 \. 232 254 … … 262 284 263 285 ALTER TABLE ONLY domains 264 ADD CONSTRAINT domains_pkey PRIMARY KEY ("domain");265 266 ALTER TABLE ONLY domains267 286 ADD CONSTRAINT domains_domain_id_key UNIQUE (domain_id); 268 287 … … 284 303 -- foreign keys 285 304 -- fixme: permissions FK refs 305 ALTER TABLE ONLY locations 306 ADD CONSTRAINT "locations_group_id_fkey" FOREIGN KEY (group_id) REFERENCES groups(group_id); 307 286 308 ALTER TABLE ONLY domains 309 ADD CONSTRAINT "$1" FOREIGN KEY (group_id) REFERENCES groups(group_id); 310 311 ALTER TABLE ONLY revzones 287 312 ADD CONSTRAINT "$1" FOREIGN KEY (group_id) REFERENCES groups(group_id); 288 313 … … 299 324 SELECT pg_catalog.setval('misc_misc_id_seq', 2, false); 300 325 SELECT pg_catalog.setval('default_records_record_id_seq', 8, false); 301 SELECT pg_catalog.setval('default_rev_records_record_id_seq', 3, false);326 SELECT pg_catalog.setval('default_rev_records_record_id_seq', 5, false); 302 327 SELECT pg_catalog.setval('domains_domain_id_seq', 1, false); 303 328 SELECT pg_catalog.setval('groups_group_id_seq', 2, false); -
branches/stable/notes
r81 r545 309 309 #define LOG_INFO 6 /* informational */ 310 310 #define LOG_DEBUG 7 /* debug-level messages */ 311 312 313 314 another web-UI for DNS record maintenance: 315 http://www.henriknordstrom.net/code/webdns/ 316 317 318 sub-octet delegation for v4 nets: 319 p 216-218 in cricket^Wgrasshopper book 320 321 Also see new draft spec, applies to both v4 and v6: 322 http://tools.ietf.org/html/draft-gersch-dnsop-revdns-cidr-01 323 324 new custom types "Forward delegation" and "Reverse delegation"? 325 - forward creates NS records in parent for <sub>.parent 326 - reverse creates NS records plus CNAMEs for sub-octet zones 327 -> would solve the conundrum of what to do with the unsightly CNAME 328 records presented in the UI to indicate sub-octet zone delegation -
branches/stable/templates/axfr.tmpl
r437 r545 39 39 </tr> 40 40 <tr class="datalinelight"> 41 <td>Merge records on A+PTR or AAAA+PTR match?</td> 42 <td><input type="checkbox" name="mergematching"<TMPL_IF mergematching> checked="checked"</TMPL_IF> /></td> 43 </tr> 44 <tr class="datalinelight"> 41 45 <td>Import as active?</td> 42 46 <td><input type="checkbox" name="domactive"<TMPL_UNLESS dominactive> checked="checked"</TMPL_UNLESS> /></td> … … 44 48 <tr class="datalinelight"> 45 49 <td valign="top">Domains to import:<br />(one per line)</td> 46 <td><textarea name="importdoms" rows="10" cols=" 25"><TMPL_IF importdoms><TMPL_VAR NAME=importdoms></TMPL_IF></textarea></td>50 <td><textarea name="importdoms" rows="10" cols="45"><TMPL_IF importdoms><TMPL_VAR NAME=importdoms></TMPL_IF></textarea></td> 47 51 </tr> 48 52 <tr class="datalinelight"> -
branches/stable/templates/bulkchange.tmpl
r113 r545 10 10 <tr class="datalinelight"> 11 11 <td><TMPL_VAR NAME=domain></td> 12 <TMPL_IF domok> <td>OK</td> 13 <TMPL_ELSE><TMPL_IF domwarn> <td class="warn">Import OK but:<br /> 14 <TMPL_VAR NAME=domwarn></td> 15 <TMPL_ELSE> <td class="err">Failed: <TMPL_VAR NAME=domerr></td> 12 <TMPL_IF domok> <td><TMPL_VAR NAME=domok></td> 13 <TMPL_ELSE><TMPL_IF domwarn> <td class="warn"><TMPL_VAR NAME=domwarn></td> 14 <TMPL_ELSE> <td class="err"><TMPL_VAR NAME=domerr></td> 16 15 </TMPL_IF></TMPL_IF> 17 16 </tr> 18 17 </TMPL_LOOP> 19 18 </table> 20 <TMPL_VAR NAME=foobar>21 19 </td> 22 20 </tr> -
branches/stable/templates/bulkdomain.tmpl
r207 r545 40 40 <table> 41 41 <tr> 42 <TMPL_LOOP NAME=domtable><td><input type="checkbox" name="dom_<TMPL_VAR NAME=dom id>" value="<TMPL_VAR NAME=domid>" /> <TMPL_VAR NAME=domain></td>42 <TMPL_LOOP NAME=domtable><td><input type="checkbox" name="dom_<TMPL_VAR NAME=domainid>" value="<TMPL_VAR NAME=domainid>" /> <TMPL_VAR NAME=domain></td> 43 43 <TMPL_IF newrow></tr> 44 44 <tr> -
branches/stable/templates/domlist.tmpl
r544 r545 5 5 <td align="center" valign="top"> 6 6 7 <TMPL_IF resultmsg> 8 <div class="result"><TMPL_VAR NAME=resultmsg></div> 9 </TMPL_IF> 10 <TMPL_IF errmsg> 11 <div class="errmsg"><TMPL_VAR NAME=errmsg></div> 12 </TMPL_IF> 7 <TMPL_INCLUDE NAME="msgblock.tmpl"> 13 8 14 9 <table width="98%"> … … 33 28 <table width="98%" border="0" cellspacing="4" cellpadding="3"> 34 29 <tr> 35 <TMPL_LOOP NAME=colheads> 36 <td class="datahead_<TMPL_IF firstcol>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR 37 NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR 38 NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR 39 NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" 40 src="images/<TMPL_VAR NAME=sortorder>.png" /></TMPL_IF></td> 30 <TMPL_LOOP NAME=colheads> <td class="datahead_<TMPL_IF __first__>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR 31 NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR 32 NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR 33 NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR 34 NAME=sortorder>.png" /></TMPL_IF></td> 41 35 </TMPL_LOOP> 42 36 <TMPL_IF domain_edit> <td class="datahead_s">Change Status</td></TMPL_IF> … … 45 39 <TMPL_IF name=domtable> 46 40 <TMPL_LOOP name=domtable> 47 <tr class="row<TMPL_IF __odd__> 1<TMPL_ELSE>0</TMPL_IF>">48 <td align="left"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=reclist&id=<TMPL_VAR NAME=domain id>&defrec=n<TMPL_UNLESS domlist>&revrec=y</TMPL_UNLESS>"><TMPL_VAR NAME=domain></a></td>41 <tr class="row<TMPL_IF __odd__>0<TMPL_ELSE>1</TMPL_IF>"> 42 <td align="left"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=reclist&id=<TMPL_VAR NAME=domain_id>&defrec=n<TMPL_UNLESS domlist>&revrec=y</TMPL_UNLESS>"><TMPL_VAR NAME=domain></a></td> 49 43 <td><TMPL_IF status>Active<TMPL_ELSE>Inactive</TMPL_IF></td> 50 44 <td><TMPL_VAR name=group></td> 51 <TMPL_IF domain_edit> <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=curpage><TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&id=<TMPL_VAR NAME=domainid>& domstatus=<TMPL_IF status>domoff<TMPL_ELSE>domon</TMPL_IF>"><TMPL_IF status>deactivate<TMPL_ELSE>activate</TMPL_IF></a></td></TMPL_IF>52 <TMPL_IF domain_delete> <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_IF domlist>deldom<TMPL_ELSE>delrevzone</TMPL_IF>&id=<TMPL_VAR NAME=domain id>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td></TMPL_IF>45 <TMPL_IF domain_edit> <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=curpage><TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&id=<TMPL_VAR NAME=domainid>&zonestatus=<TMPL_IF status>domoff<TMPL_ELSE>domon</TMPL_IF>"><TMPL_IF status>deactivate<TMPL_ELSE>activate</TMPL_IF></a></td></TMPL_IF> 46 <TMPL_IF domain_delete> <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_IF domlist>deldom<TMPL_ELSE>delrevzone</TMPL_IF>&id=<TMPL_VAR NAME=domain_id>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td></TMPL_IF> 53 47 </tr> 54 48 </TMPL_LOOP> -
branches/stable/templates/editsoa.tmpl
r100 r545 16 16 <input type="hidden" name="page" value="updatesoa" /> 17 17 <input type="hidden" name="id" value="<TMPL_VAR NAME=id>" /> 18 <input type="hidden" name="recid" value="<TMPL_VAR NAME=recid>" />19 18 <input type="hidden" name="defrec" value="<TMPL_VAR NAME=defrec>" /> 19 <input type="hidden" name="revrec" value="<TMPL_VAR NAME=revrec>" /> 20 20 21 21 <table border="0" cellspacing="2" cellpadding="1" width="100%"> 22 22 <tr class="darkrowheader"> 23 23 <td colspan="2" class="title"><TMPL_IF NAME=isgrp>Edit default SOA record for group <TMPL_ELSE>Edit SOA record for </TMPL_IF><TMPL_VAR NAME=parent></td> 24 <td class="title"> Defaults:</td>24 <td class="title">System defaults:</td> 25 25 </tr> 26 26 <tr class="datalinelight"> -
branches/stable/templates/grpman.tmpl
r178 r545 5 5 <td align="center" valign="top"> 6 6 7 <TMPL_IF resultmsg> 8 <div class="result"><TMPL_VAR NAME=resultmsg></div> 9 </TMPL_IF> 10 <TMPL_IF warnmsg> 11 <div class="warning">Warning: <TMPL_VAR NAME=warnmsg></div> 12 </TMPL_IF> 13 <TMPL_IF errmsg> 14 <div class="errmsg"><TMPL_VAR NAME=errmsg></div> 15 </TMPL_IF> 7 <TMPL_INCLUDE NAME="msgblock.tmpl"> 16 8 17 9 <table width="98%"> … … 32 24 <table width="98%" border="0" cellspacing="4" cellpadding="3"> 33 25 <tr> 34 <TMPL_LOOP NAME=colheads> 35 <td class="datahead_<TMPL_IF firstcol>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR NAME=sortorder>.png" /></TMPL_IF></td></TMPL_LOOP> 26 <TMPL_LOOP NAME=colheads> <td class="datahead_<TMPL_IF __first__>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR 27 NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR 28 NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR 29 NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR 30 NAME=sortorder>.png" /></TMPL_IF></td> 31 </TMPL_LOOP> 36 32 <TMPL_IF delgrp> 37 33 <td class="datahead_s">Delete</td> … … 40 36 <TMPL_IF name=grouptable> 41 37 <TMPL_LOOP name=grouptable> 42 <tr class="row<TMPL_ VAR name=bg>">38 <tr class="row<TMPL_IF __odd__>0<TMPL_ELSE>1</TMPL_IF>"> 43 39 <td align="left"><TMPL_IF edgrp><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=edgroup&gid=<TMPL_VAR NAME=groupid>"><TMPL_VAR NAME=groupname></a><TMPL_ELSE><TMPL_VAR NAME=groupname></TMPL_IF></td> 44 40 <td><TMPL_VAR name=pgroup></td> 45 41 <td><TMPL_VAR name=nusers></td> 46 42 <td><TMPL_VAR name=ndomains></td> 43 <td><TMPL_VAR NAME=nrevzones></td> 47 44 <TMPL_IF delgrp> 48 45 <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=delgrp&id=<TMPL_VAR NAME=groupid>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td> -
branches/stable/templates/location.tmpl
r374 r545 14 14 <input type="hidden" name="page" value="location" /> 15 15 <input type="hidden" name="sid" value="<TMPL_VAR NAME=sid>" /> 16 <input type="hidden" name="parentid" value="<TMPL_VAR NAME=parentid>" /> 17 <input type="hidden" name="id" value="<TMPL_VAR NAME=id>" /> 16 <TMPL_IF id><input type="hidden" name="id" value="<TMPL_VAR NAME=id>" /></TMPL_IF> 18 17 <input type="hidden" name="locact" value="<TMPL_VAR NAME=locact>" /> 19 18 … … 26 25 <tr class="datalinelight"> 27 26 <td>Location name/description</td> 28 <td><input type="text" name="locname" value="<TMPL_VAR ESCAPE=HTML NAME=locname>" size="30" /></td>27 <td><input type="text" name="locname" value="<TMPL_VAR ESCAPE=HTML NAME=locname>" size="30" maxlength="40" /></td> 29 28 </tr> 30 29 <tr class="datalinelight"> 31 30 <td>IP list</td> 32 31 <td><input type="text" name="iplist" value="<TMPL_VAR ESCAPE=HTML NAME=iplist>" size="30" /></td> 32 </tr> 33 <tr class="datalinelight"> 34 <td>Comments</td> 35 <td><textarea name="comments" cols="50" rows="5"><TMPL_VAR ESCAPE=HTML NAME=comments></textarea></td> 33 36 </tr> 34 37 <tr class="datalinelight"> -
branches/stable/templates/loclist.tmpl
r370 r545 5 5 <td align="center" valign="top"> 6 6 7 <TMPL_IF resultmsg> 8 <div class="result"><TMPL_VAR NAME=resultmsg></div> 9 </TMPL_IF> 10 <TMPL_IF warnmsg> 11 <div class="warning">Warning: <TMPL_VAR NAME=warnmsg></div> 12 </TMPL_IF> 13 <TMPL_IF errmsg> 14 <div class="errmsg"><TMPL_VAR NAME=errmsg></div> 15 </TMPL_IF> 7 <TMPL_INCLUDE NAME="msgblock.tmpl"> 16 8 17 9 <table width="98%" class="csubtable"> … … 22 14 <td class="rightthird"><TMPL_INCLUDE NAME="sbox.tmpl"></td> 23 15 </tr> 24 <tr><td colspan="3" align="center"><TMPL_INCLUDE NAME="lettsearch.tmpl"></td></tr>25 16 <TMPL_IF addloc> 26 <tr><td colspan="3" align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=loc ">New Location/View</a></td></tr>17 <tr><td colspan="3" align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=location">New Location/View</a></td></tr> 27 18 </TMPL_IF> 28 19 </table> … … 30 21 <table width="98%" border="0" cellspacing="4" cellpadding="3" class="csubtable"> 31 22 <tr> 32 <TMPL_LOOP NAME=colheads> 33 <td class="datahead_<TMPL_IF firstcol>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR NAME=sortorder>.png" /></TMPL_IF></td></TMPL_LOOP> 23 <TMPL_LOOP NAME=colheads> <td class="datahead_s"><a href="dns.cgi?sid=<TMPL_VAR 24 NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR 25 NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR 26 NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR 27 NAME=sortorder>.png" /></TMPL_IF></td> 28 </TMPL_LOOP> 34 29 <TMPL_IF delloc> <td class="datahead_s">Delete</td></TMPL_IF> 35 30 </tr> … … 37 32 <TMPL_LOOP name=loctable> 38 33 <tr class="row<TMPL_IF __odd__>0<TMPL_ELSE>1</TMPL_IF>"> 39 <td align="left"><TMPL_IF edloc><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=location&loc _action=edit&loc=<TMPL_VAR NAME=loc>"><TMPL_VAR NAME=description></a><TMPL_ELSE><TMPL_VAR NAME=description></TMPL_IF></td>34 <td align="left"><TMPL_IF edloc><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=location&locact=edit&loc=<TMPL_VAR NAME=location>"><TMPL_VAR NAME=description></a><TMPL_ELSE><TMPL_VAR NAME=description></TMPL_IF></td> 40 35 <td><TMPL_VAR name=iplist></td> 41 36 <td><TMPL_VAR name=group_name></td> -
branches/stable/templates/log.tmpl
r180 r545 10 10 11 11 <table border="0" width="90%"> 12 <tr><th colspan="5"><div class="center maintitle">Log entries for <TMPL_VAR NAME=logfor></div></th></tr> 13 <tr class="darkrowheader"> 14 <td>Name</td> 12 <tr><th colspan="3"><div class="center maintitle">Log entries for <TMPL_VAR NAME=logfor></div></th></tr> 13 <tr> 14 <td class="leftthird"><TMPL_INCLUDE NAME="pgcount.tmpl"></td> 15 <td align="center"><TMPL_INCLUDE NAME="fpnla.tmpl"></td> 16 <td class="rightthird"> </td> 17 </tr> 18 </table> 19 <table border="0" width="90%"> 15 20 <!-- Not sure "Customer ID" (filled with uid) is of any use... --> 16 21 <!-- td>Customer ID</td --> 17 <td>Username/Email</td> 18 <td>Log Entry</td> 19 <td>Date / Time</td> 20 </tr> 22 <tr class="darkrowheader"> 23 <TMPL_LOOP NAME=colheads> <td><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF 24 NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&sortby=<TMPL_VAR 25 NAME=sortby>&order=<TMPL_VAR NAME=order>&id=<TMPL_VAR NAME=id>&ltype=<TMPL_VAR 26 NAME=ltype>"><TMPL_VAR NAME=colname></a><TMPL_IF 27 NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR 28 NAME=sortorder>.png" /></TMPL_IF></td> 29 </TMPL_LOOP> 30 </tr> 31 21 32 <TMPL_IF logentries> 22 33 <TMPL_LOOP NAME=logentries> -
branches/stable/templates/menu.tmpl
r544 r545 9 9 <TMPL_IF maydefrec><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=reclist&id=<TMPL_VAR NAME=group>&defrec=y">Default Records</a><br /> 10 10 <TMPL_IF mayrdns><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=reclist&id=<TMPL_VAR NAME=group>&defrec=y&revrec=y">Default Reverse Records</a><br /></TMPL_IF></TMPL_IF> 11 <TMPL_IF mayloc><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=loclist&id=<TMPL_VAR NAME=group>">Locations/Views</a><br /></TMPL_IF> 11 12 <TMPL_IF mayimport><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=axfr">AXFR Import</a><br /></TMPL_IF> 12 13 <TMPL_IF maybulk><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=bulkdomain">Bulk Domain Operations</a><br /></TMPL_IF> -
branches/stable/templates/newdomain.tmpl
r205 r545 30 30 </tr> 31 31 <tr class="datalinelight"> 32 <td>Make domain active on next DNS propagation</td><td><input type="checkbox" name="makeactive" checked="checked"/></td>32 <td>Make domain active on next DNS propagation</td><td><input type="checkbox" name="makeactive"<TMPL_UNLESS addinactive> checked="checked"</TMPL_UNLESS> /></td> 33 33 </tr> 34 <TMPL_IF location_view> 35 <tr class="datalinelight"> 36 <td>Default location/view:</td> 37 <td><select name="defloc"> 38 <option value="">(None/public)</option> 39 <TMPL_LOOP name=loclist> <option value="<TMPL_VAR NAME=loc>"<TMPL_IF selected> selected="selected"</TMPL_IF>><TMPL_VAR NAME=locname></option> 40 </TMPL_LOOP> 41 </select></td> 42 </tr> 43 </TMPL_IF> 34 44 <tr><td colspan="2" class="tblsubmit"><input type="submit" value="Add domain" /></td></tr> 35 45 </table> -
branches/stable/templates/newgrp.tmpl
r207 r545 15 15 <tr><td> 16 16 <table border="0" cellspacing="2" cellpadding="2" width="100%"> 17 <TMPL_IF add_failed> <tr><td class="errhead" colspan=" 4">Error adding group <TMPL_VAR NAME=newgroup>: <TMPL_VAR NAME=errmsg></td></tr></TMPL_IF>18 <tr class="darkrowheader"><td colspan=" 4" align="center">Add Group</td></tr>17 <TMPL_IF add_failed> <tr><td class="errhead" colspan="2">Error adding group <TMPL_VAR NAME=newgroup>: <TMPL_VAR NAME=errmsg></td></tr></TMPL_IF> 18 <tr class="darkrowheader"><td colspan="2" align="center">Add Group</td></tr> 19 19 20 20 <tr class="datalinelight"> 21 <td colspan=2>Group Name:</td>22 <td align="left" colspan=2><input type="text" name="newgroup" value="<TMPL_VAR NAME=newgroup>" /></td>21 <td>Group Name:</td> 22 <td align="left"><input type="text" name="newgroup" value="<TMPL_VAR NAME=newgroup>" /></td> 23 23 </tr> 24 24 <tr class="datalinelight"> 25 <td colspan=2>Add as subgroup of:</td>26 <td colspan=2><select name="pargroup">25 <td>Add as subgroup of:</td> 26 <td><select name="pargroup"> 27 27 <TMPL_LOOP name=pargroup> <option value="<TMPL_VAR NAME=groupval>"<TMPL_IF groupactive> selected="selected"</TMPL_IF>><TMPL_VAR name=groupname></option> 28 28 </TMPL_LOOP> … … 30 30 </tr> 31 31 <tr class="darkrowheader border"> 32 <td colspan=" 4" align="center">Default permissions for users created in this group:</td>32 <td colspan="2" align="center">Default permissions for users created in this group:</td> 33 33 </tr> 34 <tr><td colspan="2"><table> 34 35 <TMPL_INCLUDE name="permlist.tmpl"> 36 </table></td></tr> 35 37 <tr class="darkrowheader"> 36 <td colspan=" 4" align="center"><input type="submit" value="Add group" /></td>38 <td colspan="2" align="center"><input type="submit" value="Add group" /></td> 37 39 </tr> 38 40 </table> -
branches/stable/templates/newrevzone.tmpl
r544 r545 15 15 <tr><td> 16 16 <table border="0" cellspacing="2" cellpadding="2" width="100%"> 17 <TMPL_IF add_failed> <tr><td class="errhead" colspan="2">Error adding reverse zone <TMPL_VAR NAME=revzone>:17 <TMPL_IF errmsg> <tr><td class="errhead" colspan="2">Error adding reverse zone <TMPL_VAR NAME=revzone>: 18 18 <TMPL_VAR NAME=errmsg></td></tr></TMPL_IF> 19 19 <tr class="darkrowheader"><td colspan="2" align="center">Add Reverse Zone</td></tr> -
branches/stable/templates/permlist.tmpl
r67 r545 22 22 <td<TMPL_UNLESS may_record_create> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="record_create"</TMPL_UNLESS><TMPL_IF record_create> checked="checked"</TMPL_IF><TMPL_UNLESS may_record_create> disabled="disabled"</TMPL_UNLESS> /> Create</td> 23 23 <td<TMPL_UNLESS may_record_delete> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="record_delete"</TMPL_UNLESS><TMPL_IF record_delete> checked="checked"</TMPL_IF><TMPL_UNLESS may_record_delete> disabled="disabled"</TMPL_UNLESS> /> Delete</td> 24 <td<TMPL_UNLESS may_record_locchg> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="record_locchg"</TMPL_UNLESS><TMPL_IF record_locchg> checked="checked"</TMPL_IF><TMPL_UNLESS may_record_locchg> disabled="disabled"</TMPL_UNLESS> /> Change location</td> 24 25 <!-- td class="noaccess"> - Delegate</td --> 26 </tr> 27 <tr> 28 <td align="right">Location/View:</td> 29 <td<TMPL_UNLESS may_location_edit> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="location_edit"</TMPL_UNLESS><TMPL_IF location_edit> checked="checked"</TMPL_IF><TMPL_UNLESS may_location_edit>disabled="disabled"</TMPL_UNLESS> /> Edit</td> 30 <td<TMPL_UNLESS may_location_create> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="location_create"</TMPL_UNLESS><TMPL_IF location_create> checked="checked"</TMPL_IF><TMPL_UNLESS may_location_create> disabled="disabled"</TMPL_UNLESS> /> Create</td> 31 <td<TMPL_UNLESS may_location_delete> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="location_delete"</TMPL_UNLESS><TMPL_IF location_delete> checked="checked"</TMPL_IF><TMPL_UNLESS may_location_delete> disabled="disabled"</TMPL_UNLESS> /> Delete</td> 32 <td<TMPL_UNLESS may_location_view> class="<TMPL_UNLESS info>noaccess<TMPL_ELSE>info</TMPL_UNLESS>"</TMPL_UNLESS>><input type="checkbox"<TMPL_UNLESS info> name="location_view"</TMPL_UNLESS><TMPL_IF location_view> checked="checked"</TMPL_IF><TMPL_UNLESS may_location_view> disabled="disabled"</TMPL_UNLESS> /> View</td> 25 33 </tr> 26 34 <tr> -
branches/stable/templates/reclist.tmpl
r544 r545 5 5 <td align="center" valign="top"> 6 6 7 <TMPL_IF resultmsg> 8 <div class="result"><TMPL_VAR NAME=resultmsg></div> 9 </TMPL_IF> 10 <TMPL_IF warnmsg> 11 <div class="warning"><TMPL_VAR NAME=warnmsg></div> 12 </TMPL_IF> 13 <TMPL_IF errmsg> 14 <div class="errmsg"><TMPL_VAR NAME=errmsg></div> 15 </TMPL_IF> 7 <TMPL_INCLUDE NAME="msgblock.tmpl"> 16 8 17 9 <TMPL_UNLESS perm_err> … … 59 51 <TMPL_IF reclist> 60 52 <tr class="darkrowheader"> 61 <TMPL_LOOP NAME=colheads><TMPL_IF firstcol></TMPL_IF> 62 <td><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF 53 <TMPL_LOOP NAME=colheads> <td><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF 63 54 NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&sortby=<TMPL_VAR 64 55 NAME=sortby>&order=<TMPL_VAR NAME=order>&id=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR 65 56 NAME=defrec>&revrec=<TMPL_VAR NAME=revrec>"><TMPL_VAR NAME=colname></a><TMPL_IF 66 NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR NAME=sortorder>.png" 67 /></TMPL_IF></td></TMPL_LOOP> 57 NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR 58 NAME=sortorder>.png" /></TMPL_IF></td> 59 </TMPL_LOOP> 68 60 <TMPL_IF record_delete> <td>Delete</td></TMPL_IF> 69 61 </tr> 70 62 <TMPL_LOOP NAME=reclist> 71 <tr class="row<TMPL_ VAR NAME=row>">63 <tr class="row<TMPL_IF __odd__>0<TMPL_ELSE>1</TMPL_IF>"> 72 64 <TMPL_IF fwdzone> 73 <td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=record&parentid=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR NAME=defrec>&revrec=<TMPL_VAR NAME=revrec>&recact=edit&id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=host></a><TMPL_ ELSE><TMPL_VAR NAME=host></TMPL_IF></td>65 <td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=record&parentid=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR NAME=defrec>&revrec=<TMPL_VAR NAME=revrec>&recact=edit&id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=host></a><TMPL_IF locname> (<TMPL_VAR NAME=locname>)</TMPL_IF><TMPL_ELSE><TMPL_VAR NAME=host><TMPL_IF locname> (<TMPL_VAR NAME=locname>)</TMPL_IF></TMPL_IF></td> 74 66 <td><TMPL_VAR NAME=type></td> 75 67 <td><TMPL_VAR NAME=val></td> … … 78 70 <td><TMPL_VAR NAME=port></td> 79 71 <TMPL_ELSE> 80 <td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=record&parentid=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR NAME=defrec>&revrec=<TMPL_VAR NAME=revrec>&recact=edit&id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=val></a><TMPL_ ELSE><TMPL_VAR NAME=val></TMPL_IF></td>72 <td><TMPL_IF record_edit><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=record&parentid=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR NAME=defrec>&revrec=<TMPL_VAR NAME=revrec>&recact=edit&id=<TMPL_VAR NAME=record_id>"><TMPL_VAR NAME=val></a><TMPL_IF locname> (<TMPL_VAR NAME=locname>)</TMPL_IF><TMPL_ELSE><TMPL_VAR NAME=val><TMPL_IF locname> (<TMPL_VAR NAME=locname>)</TMPL_IF></TMPL_IF></td> 81 73 <td><TMPL_VAR NAME=type></td> 82 74 <td><TMPL_VAR NAME=host></td> -
branches/stable/templates/record.tmpl
r544 r545 29 29 <TMPL_IF fwdzone> 30 30 <td>Hostname</td> 31 <td><input type="text" name="name" value="<TMPL_VAR NAME=name>" /></td>31 <td><input type="text" name="name" value="<TMPL_VAR NAME=name>" size="30" /></td> 32 32 <TMPL_ELSE> 33 33 <td>IP Address</td> 34 <td><input type="text" name="address" value="<TMPL_VAR ESCAPE=HTML NAME=address>" /></td>34 <td><input type="text" name="address" value="<TMPL_VAR ESCAPE=HTML NAME=address>" size="30" /></td> 35 35 </TMPL_IF> 36 36 </tr> … … 46 46 <TMPL_IF fwdzone> 47 47 <td>Address</td> 48 <td><input type="text" name="address" value="<TMPL_VAR ESCAPE=HTML NAME=address>" /></td>48 <td><input type="text" name="address" value="<TMPL_VAR ESCAPE=HTML NAME=address>" size="30" /></td> 49 49 <TMPL_ELSE> 50 50 <td>Hostname</td> 51 <td><input type="text" name="name" value="<TMPL_VAR NAME=name>" /></td>51 <td><input type="text" name="name" value="<TMPL_VAR NAME=name>" size="30" /></td> 52 52 </TMPL_IF> 53 53 </tr> … … 70 70 <td><input size="7" maxlength="20" type="text" name="ttl" value="<TMPL_VAR NAME=ttl>" /></td> 71 71 </tr> 72 <TMPL_IF location_view> 73 <tr class="datalinelight"> 74 <td>Location/view</td> 75 <TMPL_IF record_locchg> 76 <td><select name="location"> 77 <TMPL_LOOP name=loclist> <option value="<TMPL_VAR NAME=loc>"<TMPL_IF selected> selected="selected"</TMPL_IF>><TMPL_VAR NAME=locname></option> 78 </TMPL_LOOP> 79 </select></td> 80 <TMPL_ELSE> 81 <td><TMPL_VAR NAME=loc_name></td> 82 </TMPL_IF> 83 </tr> 84 </TMPL_IF> 72 85 <tr class="datalinelight"> 73 86 <td colspan="2" align="center"><input type="submit" value=" <TMPL_VAR NAME=todo> " /></td> -
branches/stable/templates/soadata.tmpl
r162 r545 3 3 <td align="left">SOA:</td> 4 4 <TMPL_IF mayeditsoa> 5 <td align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=editsoa&id=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR NAME=defrec> ">edit</a></td></TMPL_IF>5 <td align="right"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=editsoa&id=<TMPL_VAR NAME=id>&defrec=<TMPL_VAR NAME=defrec>&revrec=<TMPL_VAR NAME=revrec>">edit</a></td></TMPL_IF> 6 6 </tr> 7 7 </table> -
branches/stable/templates/user.tmpl
r207 r545 16 16 <table border="0" cellspacing="2" cellpadding="2" width="450"> 17 17 <TMPL_IF add_failed> <tr> 18 <td class="errhead" colspan="2"> Error <TMPL_IF add>adding<TMPL_ELSE>updating</TMPL_IF> user <TMPL_VAR NAME=uname>:<TMPL_VAR NAME=errmsg></td>18 <td class="errhead" colspan="2"><TMPL_VAR NAME=errmsg></td> 19 19 </tr></TMPL_IF> 20 20 <tr class="darkrowheader"><td colspan="2" align="center"><TMPL_IF add>Add<TMPL_ELSE>Edit</TMPL_IF> User</td></tr> -
branches/stable/templates/useradmin.tmpl
r207 r545 5 5 <td align="center" valign="top"> 6 6 7 <TMPL_IF resultmsg> 8 <div class="result"><TMPL_VAR NAME=resultmsg></div> 9 </TMPL_IF> 10 <TMPL_IF warnmsg> 11 <div class="warning">Warning: <TMPL_VAR NAME=warnmsg></div> 12 </TMPL_IF> 13 <TMPL_IF errmsg> 14 <div class="errmsg"><TMPL_VAR NAME=errmsg></div> 15 </TMPL_IF> 7 <TMPL_INCLUDE NAME="msgblock.tmpl"> 16 8 17 9 <table width="98%" class="csubtable"> … … 30 22 <table width="98%" border="0" cellspacing="4" cellpadding="3" class="csubtable"> 31 23 <tr> 32 <TMPL_LOOP NAME=colheads> 33 <td class="datahead_<TMPL_IF firstcol>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR NAME=sortorder>.png" /></TMPL_IF></td></TMPL_LOOP> 24 <TMPL_LOOP NAME=colheads> <td class="datahead_<TMPL_IF __first__>l<TMPL_ELSE>s</TMPL_IF>"><a href="dns.cgi?sid=<TMPL_VAR 25 NAME=sid>&page=<TMPL_VAR NAME=page><TMPL_IF NAME=offset>&offset=<TMPL_VAR 26 NAME=offset></TMPL_IF>&sortby=<TMPL_VAR NAME=sortby>&order=<TMPL_VAR NAME=order>"><TMPL_VAR 27 NAME=colname></a><TMPL_IF NAME=sortorder> <img alt="<TMPL_VAR NAME=sortorder>" src="images/<TMPL_VAR 28 NAME=sortorder>.png" /></TMPL_IF></td> 29 </TMPL_LOOP> 34 30 <TMPL_IF deluser> <td class="datahead_s">Delete</td></TMPL_IF> 35 31 </tr> 36 32 <TMPL_IF name=usertable> 37 33 <TMPL_LOOP name=usertable> 38 <tr class="row<TMPL_ VAR name=bg>">39 <td align="left"><TMPL_IF eduser><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=user&useraction=edit&user=<TMPL_VAR NAME=user id>"><TMPL_VAR NAME=username></a><TMPL_ELSE><TMPL_VAR NAME=username></TMPL_IF></td>40 <td class="data_nowrap"><TMPL_VAR name= userfull></td>41 <td><TMPL_VAR name= usertype></td>42 <td><TMPL_VAR name= usergroup></td>34 <tr class="row<TMPL_IF __odd__>0<TMPL_ELSE>1</TMPL_IF>"> 35 <td align="left"><TMPL_IF eduser><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=user&useraction=edit&user=<TMPL_VAR NAME=user_id>"><TMPL_VAR NAME=username></a><TMPL_ELSE><TMPL_VAR NAME=username></TMPL_IF></td> 36 <td class="data_nowrap"><TMPL_VAR name=fname></td> 37 <td><TMPL_VAR name=type></td> 38 <td><TMPL_VAR name=group_name></td> 43 39 <td align="center"> 44 40 <TMPL_IF eduser> 45 <a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=useradmin<TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&id=<TMPL_VAR NAME=user id>&userstatus=<TMPL_IF active>useroff<TMPL_ELSE>useron</TMPL_IF>"><TMPL_IF active>enabled<TMPL_ELSE>disabled</TMPL_IF></a>41 <a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=useradmin<TMPL_IF NAME=offset>&offset=<TMPL_VAR NAME=offset></TMPL_IF>&id=<TMPL_VAR NAME=user_id>&userstatus=<TMPL_IF status>useroff<TMPL_ELSE>useron</TMPL_IF>"><TMPL_IF status>enabled<TMPL_ELSE>disabled</TMPL_IF></a> 46 42 <TMPL_ELSE> 47 <TMPL_IF active>enabled<TMPL_ELSE>disabled</TMPL_IF>43 <TMPL_IF status>enabled<TMPL_ELSE>disabled</TMPL_IF> 48 44 </TMPL_IF> 49 45 </td> 50 46 <TMPL_IF deluser> 51 <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=deluser&id=<TMPL_VAR NAME=user id>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td>47 <td align="center"><a href="dns.cgi?sid=<TMPL_VAR NAME=sid>&page=deluser&id=<TMPL_VAR NAME=user_id>"><img src="images/trash2.png" alt="[ Delete ]" /></a></td> 52 48 </TMPL_IF> 53 49 </tr> -
branches/stable/tiny-import.pl
r348 r545 19 19 ## 20 20 21 # WARNING: This is NOT a heavy-duty validator; it is assumed that the data 22 # being imported is more or less sane. Only minor structural validation will 23 # be done to weed out the most broken records. 24 21 25 use strict; 22 26 use warnings; … … 29 33 } 30 34 35 usage() if !@ARGV; 36 37 my %importcfg = ( 38 rw => 0, 39 conv => 0, 40 trial => 0, 41 ); 42 # Handle some command-line arguments 43 while ($ARGV[0] =~ /^-/) { 44 my $arg = shift @ARGV; 45 usage() if $arg !~ /^-[rct]+$/; 46 # -r rewrite imported files to comment imported records 47 # -c coerce/downconvert A+PTR = records to PTR 48 # -t trial mode; don't commit to DB or actually rewrite flatfile (disables -r) 49 $arg =~ s/^-//; 50 my @tmp = split //, $arg; 51 foreach (@tmp) { 52 $importcfg{rw} = 1 if $_ eq 'r'; 53 $importcfg{conv} = 1 if $_ eq 'c'; 54 $importcfg{trial} = 1 if $_ eq 't'; 55 } 56 } 57 $importcfg{rw} = 0 if $importcfg{trial}; 58 59 sub usage { 60 die q(usage: tiny-import.pl [-r] [-c] datafile1 datafile2 ... datafileN ... 61 -r Rewrite all specified data files with a warning header indicating the 62 records are now managed by web, and commenting out all imported records. 63 The directory containing any given datafile must be writable. 64 -c Convert any A+PTR (=) record to a bare PTR if the forward domain is 65 not present in the database. Note this does NOT look forward through 66 a single file, nor across multiple files handled in the same run. 67 Multiple passes may be necessary if SOA and = records are heavily 68 intermixed and not clustered together. 69 -t Trial run mode; spits out records that would be left unimported. 70 Disables -r if set. 71 72 -r and -c may be combined (-rc) 73 74 datafileN is any tinydns record data file. 75 ); 76 } 77 31 78 my $code; 32 79 my ($dbh,$msg) = connectDB($config{dbname}, $config{dbuser}, $config{dbpass}, $config{dbhost}); … … 38 85 my %cnt; 39 86 my @deferred; 87 my $errstr = ''; 40 88 41 89 foreach my $file (@ARGV) { … … 43 91 import(file => $file); 44 92 # import(file => $file, nosoa => 1); 45 $dbh->rollback ;46 # $dbh->commit;93 $dbh->rollback if $importcfg{trial}; 94 $dbh->commit unless $importcfg{trial}; 47 95 }; 48 96 if ($@) { 49 print "bleh: $@\n"; 50 die "die harder\n"; 97 print "Failure trying to import $file: $@\n $errstr\n"; 98 unlink ".$file.$$" if $importcfg{rw}; # cleanup 99 $dbh->rollback; 51 100 } 52 101 } 53 102 54 foreach (keys %cnt) { 55 print " $_ $cnt{$_}\n"; 56 } 103 # print summary count of record types encountered 104 foreach (keys %cnt) { 105 print " $_ $cnt{$_}\n"; 106 } 57 107 58 108 exit 0; … … 61 111 our %args = @_; 62 112 my $flatfile = $args{file}; 113 my @fpath = split '/', $flatfile; 114 $fpath[$#fpath] = ".$fpath[$#fpath]"; 115 my $rwfile = join('/', @fpath);#.".$$"; 116 63 117 open FLAT, "<$flatfile"; 64 118 65 our $recsth = $dbh->prepare("INSERT INTO records (domain_id,rdns_id,host,type,val,distance,weight,port,ttl) ". 66 " VALUES (?,?,?,?,?,?,?,?,?)"); 67 119 if ($importcfg{rw}) { 120 open RWFLAT, ">$rwfile" or die "Couldn't open tempfile $rwfile for rewriting: $!\n"; 121 print RWFLAT "# WARNING: Records in this file have been imported to the web UI.\n#\n"; 122 } 123 124 our $recsth = $dbh->prepare("INSERT INTO records (domain_id,rdns_id,host,type,val,distance,weight,port,ttl,location) ". 125 " VALUES (?,?,?,?,?,?,?,?,?,?)"); 126 127 my %deleg; 128 129 my $ok = 0; 68 130 while (<FLAT>) { 69 next if /^#/; 70 next if /^\s*$/; 131 if (/^#/ || /^\s*$/) { 132 print RWFLAT "#$_" if $importcfg{rw}; 133 next; 134 } 71 135 chomp; 72 recslurp($_); 73 } 74 75 # Try the deferred records again, once. 136 s/\s*$//; 137 my $recstat = recslurp($_); 138 $ok++ if $recstat; 139 if ($importcfg{rw}) { 140 if ($recstat) { 141 print RWFLAT "#$_\n"; 142 } else { 143 print RWFLAT "$_\n"; 144 } 145 } 146 } 147 148 # Move the rewritten flatfile in place of the original, so that any 149 # external export processing will pick up any remaining records. 150 if ($importcfg{rw}) { 151 close RWFLAT; 152 rename "$rwfile", $flatfile; 153 } 154 155 # Show the failed records 76 156 foreach (@deferred) { 77 # print "trying $_ again\n"; 78 recslurp($_, 1); 79 } 157 print "failed to import $_\n"; 158 } 159 160 ##fixme: hmm. can't write the record back to the flatfile in the 161 # main while above, then come down here and import it anyway, can we? 162 # # Try the deferred records again, once. 163 # foreach (@deferred) { 164 # print "trying $_ again\n"; 165 # recslurp($_, 1); 166 # } 167 168 # .. but we can at least say how many records weren't imported. 169 print "$ok OK, ".scalar(@deferred)." deferred records in $flatfile\n"; 170 $#deferred = -1; 171 80 172 81 173 # Sub for various nonstandard types with lots of pure bytes expressed in octal 82 # Takes a tinydns rdata string and count, returns a lis of $count bytes as well174 # Takes a tinydns rdata string and count, returns a list of $count bytes as well 83 175 # as trimming those logical bytes off the front of the rdata string. 84 176 sub _byteparse { … … 103 195 } 104 196 197 # Convert octal-coded bytes back to something resembling normal characters, general case 198 sub _deoctal { 199 my $targ = shift; 200 while ($$targ =~ /\\(\d{3})/) { 201 my $sub = chr(oct($1)); 202 $$targ =~ s/\\$1/$sub/g; 203 } 204 } 205 206 sub _rdata2string { 207 my $rdata = shift; 208 my $tmpout = ''; 209 while ($rdata) { 210 my $bytecount = 0; 211 if ($rdata =~ /^\\/) { 212 ($bytecount) = ($rdata =~ /^(\\\d{3})/); 213 $bytecount =~ s/\\/0/; 214 $bytecount = oct($bytecount); 215 $rdata =~ s/^\\\d{3}//; 216 } else { 217 ($bytecount) = ($rdata =~ /^(.)/); 218 $bytecount = ord($bytecount); 219 $rdata =~ s/^.//; 220 } 221 my @tmp = _byteparse(\$rdata, $bytecount); 222 foreach (@tmp) { $tmpout .= chr($_); } 223 ##fixme: warn or fail on long (>256? >512? >321?) strings 224 } 225 return $tmpout; 226 } 227 228 sub _rdata2hex { 229 my $rdata = shift; 230 my $tmpout = ''; 231 while ($rdata) { 232 my $byte = ''; 233 if ($rdata =~ /^\\/) { 234 ($byte) = ($rdata =~ /^(\\\d{3})/); 235 $byte =~ s/\\/0/; 236 $tmpout .= sprintf("%0.2x", oct($byte)); 237 $rdata =~ s/^\\\d{3}//; 238 } else { 239 ($byte) = ($rdata =~ /^(.)/); 240 $tmpout .= sprintf("%0.2x", ord($byte)); 241 $rdata =~ s/^.//; 242 } 243 } 244 return $tmpout; 245 } 246 247 105 248 sub recslurp { 106 249 my $rec = shift; 107 250 my $nodefer = shift || 0; 251 my $impok = 1; 252 253 $errstr = $rec; # this way at least we have some idea what went <splat> 108 254 109 255 if ($rec =~ /^=/) { 110 256 $cnt{APTR}++; 111 if ($rec !~ /^=(?:\*|\\052)?[a-z0-9\._-]+:[\d\.]+:\d*/i) { 112 print "bad A+PTR $rec\n"; 113 return; 114 #=sud-rr-iGi0-1_sud-gw1-iGi4-2.vianet.ca::10.10.10.13:900::in 115 } 116 my ($host,$ip,$ttl,$time,$loc) = split /:/, $rec; 257 258 ##fixme: do checks like this for all types 259 if ($rec !~ /^=(?:\*|\\052)?[a-z0-9\._-]+:[\d\.]+:\d*/i) { 260 print "bad A+PTR $rec\n"; 261 return; 262 } 263 my ($host,$ip,$ttl,$stamp,$loc) = split /:/, $rec, 5; 117 264 $host =~ s/^=//; 118 265 $host =~ s/\.$//; 119 $time = '' if !$time; 120 $loc = '' if !$loc; 121 print "bleh, bad A+PTR! $rec\n" if $loc =~ /:/; 266 $ttl = 0 if !$ttl; 267 $stamp = '' if !$stamp; 268 $loc = '' if !$loc; 269 $loc = '' if $loc =~ /^:+$/; 122 270 my $fparent = DNSDB::_hostparent($dbh, $host); 123 271 my ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ($ip)); 124 272 if ($fparent && $rparent) { 125 $dbh->do("INSERT INTO records (domain_id,rdns_id,host,type,val,ttl) VALUES (?,?,?,?,?,?)", undef, 126 ($fparent, $rparent, $host, 65280, $ip, $ttl)); 273 $recsth->execute($fparent, $rparent, $host, 65280, $ip, 0, 0, 0, $ttl, $loc); 127 274 } else { 128 275 push @deferred, $rec unless $nodefer; 276 $impok = 0; 129 277 # print "$tmporig deferred; can't find both forward and reverse zone parents\n"; 130 278 } … … 132 280 } elsif ($rec =~ /^C/) { 133 281 $cnt{CNAME}++; 134 my ($host,$targ,$ttl,$time,$loc) = split /:/, $rec; 282 283 my ($host,$targ,$ttl,$stamp,$loc) = split /:/, $rec, 5; 135 284 $host =~ s/^C//; 136 285 $host =~ s/\.$//; 137 $time = '' if !$time; 138 $loc = '' if !$loc; 139 my $fparent = DNSDB::_hostparent($dbh, $host); 140 if ($fparent) { 141 142 } else { 143 push @deferred, $rec unless $nodefer; 144 # print "$tmporig deferred; can't find parent zone\n"; 286 $host =~ s/^\\052/*/; 287 $ttl = 0 if !$ttl; 288 $stamp = '' if !$stamp; 289 $loc = '' if !$loc; 290 $loc = '' if $loc =~ /^:+$/; 291 if ($host =~ /\.arpa$/) { 292 ($code,$msg) = DNSDB::_zone2cidr($host); 293 my ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ($msg)); 294 $recsth->execute(0, $rparent, $targ, 5, $msg->addr, 0, 0, 0, $ttl, $loc); 295 296 ##fixme: automagically convert manually maintained sub-/24 delegations 297 # my ($subip, $zone) = split /\./, $targ, 2; 298 # ($code, $msg) = DNSDB::_zone2cidr($zone); 299 # push @{$deleg{"$msg"}{iplist}}, $subip; 300 #print "$msg $subip\n"; 301 302 } else { 303 my $fparent = DNSDB::_hostparent($dbh, $host); 304 if ($fparent) { 305 $recsth->execute($fparent, 0, $host, 5, $targ, 0, 0, 0, $ttl, $loc); 306 } else { 307 push @deferred, $rec unless $nodefer; 308 $impok = 0; 309 # print "$tmporig deferred; can't find parent zone\n"; 310 } 145 311 } 146 312 147 313 } elsif ($rec =~ /^\&/) { 148 314 $cnt{NS}++; 315 316 my ($zone,$ip,$ns,$ttl,$stamp,$loc) = split /:/, $rec, 6; 317 $zone =~ s/^\&//; 318 $zone =~ s/\.$//; 319 $ns =~ s/\.$//; 320 $ns = "$ns.ns.$zone" if $ns !~ /\./; 321 $ttl = 0 if !$ttl; 322 $stamp = '' if !$stamp; 323 $loc = '' if !$loc; 324 $loc = '' if $loc =~ /^:+$/; 325 if ($zone =~ /\.arpa$/) { 326 ($code,$msg) = DNSDB::_zone2cidr($zone); 327 my ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >>= ?", undef, ("$msg")); 328 ##fixme, in concert with the CNAME check for same; automagically 329 # create "delegate" record instead for subzone NSes: convert above to use = instead of >>= 330 # ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ("$msg")) 331 # if !$rparent; 332 if ($rparent) { 333 $recsth->execute(0, $rparent, $ns, 2, $msg, 0, 0, 0, $ttl, $loc); 334 } else { 335 push @deferred, $rec unless $nodefer; 336 $impok = 0; 337 } 338 } else { 339 my $fparent = DNSDB::_hostparent($dbh, $zone); 340 if ($fparent) { 341 $recsth->execute($fparent, 0, $zone, 2, $ns, 0, 0, 0, $ttl, $loc); 342 $recsth->execute($fparent, 0, $ns, 2, $ip, 0, 0, 0, $ttl, $loc) if $ip; 343 } else { 344 push @deferred, $rec unless $nodefer; 345 $impok = 0; 346 } 347 } 348 149 349 } elsif ($rec =~ /^\^/) { 150 350 $cnt{PTR}++; 351 352 my ($rip,$host,$ttl,$stamp,$loc) = split /:/, $rec, 5; 353 $rip =~ s/^\^//; 354 $rip =~ s/\.$//; 355 $ttl = 0 if !$ttl; 356 $stamp = '' if !$stamp; 357 $loc = '' if !$loc; 358 $loc = '' if $loc =~ /^:+$/; 359 my $rparent; 360 if (my ($i, $z) = ($rip =~ /^(\d+)\.(\d+-(?:\d+\.){4}in-addr.arpa)$/) ) { 361 ($code,$msg) = DNSDB::_zone2cidr($z); 362 # Exact matches only, because we're in a sub-/24 delegation 363 ##fixme: flag the type of delegation (range, subnet-with-dash, subnet-with-slash) 364 # somewhere so we can recover it on export. probably best to do that in the revzone data. 365 ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet = ?", undef, ("$msg")); 366 $z =~ s/^[\d-]+//; 367 ($code,$msg) = DNSDB::_zone2cidr("$i.$z"); # Get the actual IP and normalize 368 } else { 369 ($code,$msg) = DNSDB::_zone2cidr($rip); 370 ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ("$msg")); 371 } 372 if ($rparent) { 373 $recsth->execute(0, $rparent, $host, 12, $msg->addr, 0, 0, 0, $ttl, $loc); 374 } else { 375 push @deferred, $rec unless $nodefer; 376 $impok = 0; 377 } 378 151 379 } elsif ($rec =~ /^\+/) { 152 380 $cnt{A}++; 381 382 my ($host,$ip,$ttl,$stamp,$loc) = split /:/, $rec, 5; 383 $host =~ s/^\+//; 384 $host =~ s/\.$//; 385 $host =~ s/^\\052/*/; 386 $ttl = 0 if !$ttl; 387 $stamp = '' if !$stamp; 388 $loc = '' if !$loc; 389 $loc = '' if $loc =~ /^:+$/; 390 391 my $domid = DNSDB::_hostparent($dbh, $host); 392 if ($domid) { 393 $recsth->execute($domid, 0, $host, 1, $ip, 0, 0, 0, $ttl, $loc); 394 } else { 395 push @deferred, $rec unless $nodefer; 396 $impok = 0; 397 } 398 153 399 } elsif ($rec =~ /^Z/) { 154 400 $cnt{SOA}++; 155 #Z128.91.209.in-addr.arpa:ns1.vianet.ca.:dnsadmin.vianet.ca.::1209600:1209600:900:900:900: 156 my ($zone,$master,$contact,$serial,$refresh,$retry,$expire,$minttl,$ttl,$ time,$loc) = split /:/, $rec;401 402 my ($zone,$master,$contact,$serial,$refresh,$retry,$expire,$minttl,$ttl,$stamp,$loc) = split /:/, $rec, 11; 157 403 $zone =~ s/^Z//; 158 404 $zone =~ s/\.$//; 159 405 $master =~ s/\.$//; 160 406 $contact =~ s/\.$//; 161 $time = '' if !$time; 162 $loc = '' if !$loc; 407 $ttl = 0 if !$ttl; 408 $stamp = '' if !$stamp; 409 $loc = '' if !$loc; 410 $loc = '' if $loc =~ /^:+$/; 163 411 if ($zone =~ /\.arpa$/) { 164 412 ($code,$msg) = DNSDB::_zone2cidr($zone); 165 $dbh->do("INSERT INTO revzones (revnet,group_id,status) VALUES (?,1,1)", undef, ($msg)); 413 $dbh->do("INSERT INTO revzones (revnet,group_id,status,default_location) VALUES (?,1,1,?)", 414 undef, ($msg, $loc)); 166 415 my ($rdns) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); 167 $dbh->do("INSERT INTO records (rdns_id,host,type,val,ttl) VALUES (?,?,6,?,?)", undef, 168 ($rdns, "$contact:$master", "$refresh:$retry:$expire:$minttl", $ttl)); 169 } else { 170 $dbh->do("INSERT INTO domains (domain,group_id,status) VALUES (?,1,1)", undef, ($zone));416 $recsth->execute(0, $rdns, "$contact:$master", 6, "$refresh:$retry:$expire:$minttl", 0, 0, 0, $ttl, $loc); 417 } else { 418 $dbh->do("INSERT INTO domains (domain,group_id,status,default_location) VALUES (?,1,1,?)", 419 undef, ($zone, $loc)); 171 420 my ($domid) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')"); 172 $dbh->do("INSERT INTO records (rdns_id,host,type,val,ttl) VALUES (?,?,6,?,?)", undef, 173 ($domid, "$contact:$master", "$refresh:$retry:$expire:$minttl", $ttl)); 421 $recsth->execute($domid, 0, "$contact:$master", 6, "$refresh:$retry:$expire:$minttl", 0, 0, 0, $ttl, $loc); 174 422 } 175 423 176 424 } elsif ($rec =~ /^\@/) { 177 425 $cnt{MX}++; 426 427 my ($zone,$ip,$host,$dist,$ttl,$stamp,$loc) = split /:/, $rec, 7; 428 $zone =~ s/^\@//; 429 $zone =~ s/\.$//; 430 $zone =~ s/^\\052/*/; 431 $host =~ s/\.$//; 432 $host = "$host.mx.$zone" if $host !~ /\./; 433 $ttl = 0 if !$ttl; 434 $stamp = '' if !$stamp; 435 $loc = '' if !$loc; 436 $loc = '' if $loc =~ /^:+$/; 437 438 # note we don't check for reverse domains here, because MX records don't make any sense in reverse zones. 439 # if this really ever becomes an issue for someone it can be expanded to handle those weirdos 440 441 # allow for subzone MXes, since it's perfectly legitimate to simply stuff it all in a single parent zone 442 my $domid = DNSDB::_hostparent($dbh, $zone); 443 if ($domid) { 444 $recsth->execute($domid, 0, $zone, 15, $host, $dist, 0, 0, $ttl, $loc); 445 $recsth->execute($domid, 0, $host, 1, $ip, 0, 0, 0, $ttl, $loc) if $ip; 446 } else { 447 push @deferred, $rec unless $nodefer; 448 $impok = 0; 449 } 450 178 451 } elsif ($rec =~ /^'/) { 179 452 $cnt{TXT}++; 180 453 181 sub _deoctal { 182 my $targ = shift; 183 while ($$targ =~ /\\(\d{3})/) { 184 my $sub = chr(oct($1)); 185 $$targ =~ s/\\$1/$sub/g; 186 } 187 } 188 189 my ($fqdn, $rdata, $ttl, $time, $loc) = split /:/, $rec; 454 my ($fqdn, $rdata, $ttl, $stamp, $loc) = split /:/, $rec, 5; 190 455 $fqdn =~ s/^'//; 456 $fqdn =~ s/^\\052/*/; 191 457 _deoctal(\$rdata); 192 193 print "$fqdn TXT '$rdata'\n" if $fqdn =~ /^\*/; 194 my $domid = DNSDB::_hostparent($dbh, $fqdn); 195 if ($domid) { 196 $recsth->execute($domid, 0, $fqdn, 16, $rdata, 0, 0, 0, $ttl); 197 } else { 198 push @deferred, $rec unless $nodefer; 458 $ttl = 0 if !$ttl; 459 $stamp = '' if !$stamp; 460 $loc = '' if !$loc; 461 $loc = '' if $loc =~ /^:+$/; 462 463 if ($fqdn =~ /\.arpa$/) { 464 ($code,$msg) = DNSDB::_zone2cidr($fqdn); 465 my ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ($msg)); 466 $recsth->execute(0, $rparent, $rdata, 16, "$msg", 0, 0, 0, $ttl, $loc); 467 } else { 468 my $domid = DNSDB::_hostparent($dbh, $fqdn); 469 if ($domid) { 470 $recsth->execute($domid, 0, $fqdn, 16, $rdata, 0, 0, 0, $ttl, $loc); 471 } else { 472 push @deferred, $rec unless $nodefer; 473 $impok = 0; 474 } 199 475 } 200 476 201 477 } elsif ($rec =~ /^\./) { 202 478 $cnt{NSASOA}++; 479 480 my ($fqdn, $ip, $ns, $ttl, $stamp, $loc) = split /:/, $rec, 6; 481 $fqdn =~ s/^\.//; 482 $fqdn =~ s/\.$//; 483 $ns =~ s/\.$//; 484 $ns = "$ns.ns.$fqdn" if $ns !~ /\./; 485 $ttl = 0 if !$ttl; 486 $stamp = '' if !$stamp; 487 $loc = '' if !$loc; 488 $loc = '' if $loc =~ /^:+$/; 489 490 if ($fqdn =~ /\.arpa$/) { 491 ($code,$msg) = DNSDB::_zone2cidr($fqdn); 492 my ($rdns) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet = ?", undef, ($msg)); 493 if (!$rdns) { 494 $errstr = "adding revzone $msg"; 495 $dbh->do("INSERT INTO revzones (revnet,group_id,status,default_location) VALUES (?,1,1,?)", 496 undef, ($msg, $loc)); 497 ($rdns) = $dbh->selectrow_array("SELECT currval('revzones_rdns_id_seq')"); 498 # this would probably make a lot more sense to do hostmaster.$config{admindomain} 499 $recsth->execute(0, $rdns, "hostmaster.$fqdn:$ns", 6, "16384:2048:1048576:2560", 0, 0, 0, "2560", $loc); 500 } 501 $recsth->execute(0, $rdns, $ns, 2, "$msg", 0, 0, 0, $ttl, $loc); 502 ##fixme: (?) implement full conversion of tinydns . records? 503 # -> problem: A record for NS must be added to the appropriate *forward* zone, not the reverse 504 #$recsth->execute(0, $rdns, $ns, 1, $ip, 0, 0, 0, $ttl) 505 # ... auto-A-record simply does not make sense in reverse zones. Functionally 506 # I think it would work, sort of, but it's a nasty mess and anyone hosting reverse 507 # zones has names for their nameservers already. 508 # Even the auto-nameserver-fqdn comes out... ugly. 509 510 } else { 511 my ($domid) = $dbh->selectrow_array("SELECT domain_id FROM domains WHERE lower(domain) = lower(?)", 512 undef, ($fqdn)); 513 if (!$domid) { 514 $errstr = "adding domain $fqdn"; 515 $dbh->do("INSERT INTO domains (domain,group_id,status,default_location) VALUES (?,1,1,?)", 516 undef, ($fqdn, $loc)); 517 ($domid) = $dbh->selectrow_array("SELECT currval('domains_domain_id_seq')"); 518 $recsth->execute($domid, 0, "hostmaster.$fqdn:$ns", 6, "16384:2048:1048576:2560", 0, 0, 0, "2560", $loc); 519 } 520 $recsth->execute($domid, 0, $fqdn, 2, $ns, 0, 0, 0, $ttl, $loc); 521 $recsth->execute($domid, 0, $ns, 1, $ip, 0, 0, 0, $ttl, $loc) if $ip; 522 } 523 524 525 } elsif ($rec =~ /^\%/) { 526 $cnt{VIEWS}++; 527 528 # unfortunate that we don't have a guaranteed way to get a description on these. :/ 529 my ($loc,$cnet) = split /:/, $rec, 2; 530 $loc =~ s/^\%//; 531 if (my ($iplist) = $dbh->selectrow_array("SELECT iplist FROM locations WHERE location = ?", undef, ($loc))) { 532 if ($cnet) { 533 $iplist .= ", $cnet"; 534 $dbh->do("UPDATE locations SET iplist = ? WHERE location = ?", undef, ($iplist, $loc)); 535 } else { 536 # hmm. spit out a warning? if we already have entries for $loc, adding a null 537 # entry will almost certainly Do The Wrong Thing(TM) 538 } 539 } else { 540 $cnet = '' if !$cnet; # de-nullify 541 $dbh->do("INSERT INTO locations (location,iplist,description) VALUES (?,?,?)", undef, ($loc, $cnet, $loc)); 542 } 543 203 544 } elsif ($rec =~ /^:/) { 204 545 $cnt{NCUST}++; … … 206 547 # recognition and handling for the core common types, this must deal with the leftovers. 207 548 # :fqdn:type:rdata:ttl:time:loc 208 #:mx2.sys.vianet.ca:44:\001\001\215\272\152\152\123\142\120\071\320\106\160\364\107\372\153\116\036\111\247\135:900::sys 209 #:_sipfederationtls._tcp.ncstechnology.com:33:\000\144\000\001\023\305\006sipfed\006online\004lync\003com\000:3600:: 210 211 my (undef, $fqdn, $type, $rdata, $ttl, $time, $loc) = split /:/, $rec; 212 213 if ($type == 33) { 214 # SRV 215 my ($prio, $weight, $port, $target) = (0,0,0,0); 216 217 my @tmp = _byteparse(\$rdata, 2); 218 $prio = $tmp[0] * 256 + $tmp[1]; 219 @tmp = _byteparse(\$rdata, 2); 220 $weight = $tmp[0] * 256 + $tmp[1]; 221 @tmp = _byteparse(\$rdata, 2); 222 $port = $tmp[0] * 256 + $tmp[1]; 223 224 $rdata =~ s/\\\d{3}/./g; 225 ($target) = ($rdata =~ /^\.(.+)\.$/); 549 550 my (undef, $fqdn, $type, $rdata, $ttl, $stamp, $loc) = split /:/, $rec, 7; 551 $fqdn =~ s/\.$//; 552 $fqdn =~ s/^\\052/*/; 553 $ttl = 0 if !$ttl; 554 $stamp = '' if !$stamp; 555 $loc = '' if !$loc; 556 $loc = '' if $loc =~ /^:+$/; 557 558 if ($type == 33) { 559 # SRV 560 my ($prio, $weight, $port, $target) = (0,0,0,0); 561 562 my @tmp = _byteparse(\$rdata, 2); 563 $prio = $tmp[0] * 256 + $tmp[1]; 564 @tmp = _byteparse(\$rdata, 2); 565 $weight = $tmp[0] * 256 + $tmp[1]; 566 @tmp = _byteparse(\$rdata, 2); 567 $port = $tmp[0] * 256 + $tmp[1]; 568 569 $rdata =~ s/\\\d{3}/./g; 570 ($target) = ($rdata =~ /^\.(.+)\.$/); 226 571 # hmm. the above *should* work, but What If(TM) we have ASCII-range bytes 227 572 # representing the target's fqdn part length(s)? axfr-get doesn't seem to, … … 236 581 # } 237 582 238 my $domid = DNSDB::_hostparent($dbh, $fqdn); 239 if ($domid) { 240 $recsth->execute($domid, 0, $fqdn, $type, $target, $prio, $weight, $port, $ttl) if $domid; 241 } else { 242 push @deferred, $rec unless $nodefer; 243 } 244 245 } elsif ($type == 28) { 246 # AAAA 247 248 my @v6; 249 250 for (my $i=0; $i < 8; $i++) { 251 my @tmp = _byteparse(\$rdata, 2); 252 push @v6, sprintf("%0.4x", $tmp[0] * 256 + $tmp[1]); 253 } 254 my $val = NetAddr::IP->new(join(':', @v6)); 255 256 my $domid = DNSDB::_hostparent($dbh, $fqdn); 257 if ($domid) { 258 $recsth->execute($domid, 0, $fqdn, $type, $val->addr, 0, 0, 0, $ttl) if $domid; 259 } else { 260 push @deferred, $rec unless $nodefer; 261 } 262 263 } else { 264 # ... uhhh, dunno 265 } 583 my $domid = DNSDB::_hostparent($dbh, $fqdn); 584 if ($domid) { 585 $recsth->execute($domid, 0, $fqdn, 33, $target, $prio, $weight, $port, $ttl, $loc) if $domid; 586 } else { 587 push @deferred, $rec unless $nodefer; 588 $impok = 0; 589 } 590 591 } elsif ($type == 28) { 592 # AAAA 593 my @v6; 594 595 for (my $i=0; $i < 8; $i++) { 596 my @tmp = _byteparse(\$rdata, 2); 597 push @v6, sprintf("%0.4x", $tmp[0] * 256 + $tmp[1]); 598 } 599 my $val = NetAddr::IP->new(join(':', @v6)); 600 601 my $fparent = DNSDB::_hostparent($dbh, $fqdn); 602 if ($fparent) { 603 $recsth->execute($fparent, 0, $fqdn, 28, $val->addr, 0, 0, 0, $ttl, $loc); 604 } else { 605 push @deferred, $rec unless $nodefer; 606 $impok = 0; 607 } 608 609 } elsif ($type == 16) { 610 # TXT 611 my $txtstring = _rdata2string($rdata); 612 613 if ($fqdn =~ /\.arpa$/) { 614 ($code,$msg) = DNSDB::_zone2cidr($fqdn); 615 my ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ($msg)); 616 if ($rparent) { 617 $recsth->execute(0, $rparent, $txtstring, 16, "$msg", 0, 0, 0, $ttl, $loc); 618 } else { 619 push @deferred, $rec unless $nodefer; 620 $impok = 0; 621 } 622 } else { 623 my $domid = DNSDB::_hostparent($dbh, $fqdn); 624 if ($domid) { 625 $recsth->execute($domid, 0, $fqdn, 16, $txtstring, 0, 0, 0, $ttl, $loc); 626 } else { 627 push @deferred, $rec unless $nodefer; 628 $impok = 0; 629 } 630 } 631 632 } elsif ($type == 17) { 633 # RP 634 my ($email, $txtrec) = split /\\000/, $rdata; 635 $email =~ s/\\\d{3}/./g; 636 $email =~ s/^\.//; 637 $txtrec =~ s/\\\d{3}/./g; 638 $txtrec =~ s/^\.//; 639 640 # these might actually make sense in a reverse zone... sort of. 641 if ($fqdn =~ /\.arpa$/) { 642 ($code,$msg) = DNSDB::_zone2cidr($fqdn); 643 my ($rparent) = $dbh->selectrow_array("SELECT rdns_id FROM revzones WHERE revnet >> ?", undef, ($msg)); 644 if ($rparent) { 645 $recsth->execute(0, $rparent, "$email $txtrec", 17, "$msg", 0, 0, 0, $ttl, $loc); 646 } else { 647 push @deferred, $rec unless $nodefer; 648 $impok = 0; 649 } 650 } else { 651 my $domid = DNSDB::_hostparent($dbh, $fqdn); 652 if ($domid) { 653 $recsth->execute($domid, 0, $fqdn, 17, "$email $txtrec", 0, 0, 0, $ttl, $loc); 654 } else { 655 push @deferred, $rec unless $nodefer; 656 $impok = 0; 657 } 658 } 659 660 } elsif ($type == 44) { 661 # SSHFP 662 my $sshfp = _byteparse(\$rdata, 1); 663 $sshfp .= " "._byteparse(\$rdata, 1); 664 $sshfp .= " "._rdata2hex($rdata); 665 666 # these do not make sense in a reverse zone, since they're logically attached to an A record 667 my $domid = DNSDB::_hostparent($dbh, $fqdn); 668 if ($domid) { 669 $recsth->execute($domid, 0, $fqdn, 44, $sshfp, 0, 0, 0, $ttl, $loc); 670 } else { 671 push @deferred, $rec unless $nodefer; 672 $impok = 0; 673 } 674 675 } else { 676 print "unhandled rec $rec\n"; 677 $impok = 0; 678 # ... uhhh, dunno 679 } 266 680 267 681 } else { 268 682 $cnt{other}++; 269 print " $_\n";683 print " $_\n"; 270 684 } 271 } 685 686 return $impok; # just to make sure 687 } # recslurp() 272 688 273 689 close FLAT; -
branches/stable/vega-import.pl
r263 r545 3 3 ## 4 4 # $Id$ 5 # Copyright 20 08-2011Kris Deugau <kdeugau@deepnet.cx>5 # Copyright 2011,2012 Kris Deugau <kdeugau@deepnet.cx> 6 6 # 7 7 # This program is free software: you can redistribute it and/or modify
Note:
See TracChangeset
for help on using the changeset viewer.