commit 259ffdc8ed9deab80fa9b94f252951f14a23811b Author: Nick Thomas Date: Sat Jan 10 02:01:24 2015 +0000 Initial commit diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e970a86 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +go-pdns +******* +Simple Go libraries to interact with PowerDNS ( https://www.powerdns.com/ ). + +Currently contains an implementation of the powerdns-pipebackend protocol and a +library to ease developing backends in Go. See pipe/dsl/dsl.go for usage +examples. + +APIs / etc are not set in stone yet, patches welcome. + +Contact: Nick Thomas + diff --git a/pipe/backend/backend.go b/pipe/backend/backend.go new file mode 100644 index 0000000..fb1cd4d --- /dev/null +++ b/pipe/backend/backend.go @@ -0,0 +1,174 @@ +// Copyright 2015 Bytemark Computer Consulting Ltd. All rights reserved +// Licensed under the GNU General Public License, version 2. See the LICENSE +// file for more details + +// Handler for the PowerDNS pipebackend protocol, as documented here: +// https://doc.powerdns.com/md/authoritative/backend-pipe/ +// +// Can speak all three (at time of writing) protocol versions. +// +// Usage: +// +// backend := backend.New( + +package backend + +import ( + "bufio" + "errors" + "fmt" + "io" + "strconv" + "strings" +) + +type Backend struct { + // The text the backend will serve to a successful hello message + Banner string + + // The protocol version negotiated with the remote end + ProtocolVersion int + + io *bufio.ReadWriter +} + +// A callback of this type is executed whenever a query is received. If an error +// is returned, the responses are ignored and the error text is returned to the +// backend. Otherwise, the responses are serialised and sent back in order. +type Callback func(b *Backend, q *Query) ([]*Response, error) + +// Build a new backend object. The banner is reported to the client upon +// successful negotiation; the io can be anything. +func New(r io.Reader, w io.Writer, banner string) *Backend { + io := bufio.NewReadWriter( + bufio.NewReader(r), + bufio.NewWriter(w), + ) + return &Backend{Banner: banner, io: io} +} + +// Does initial handshake with peer. Returns nil, or an error +// Note that the pipebackend protocol documentation states that if negotiation +// fails, the process should retry, not exit itself. +func (b *Backend) Negotiate() error { + hello, err := b.io.ReadString('\n') + if err != nil { + return err + } + + // We're not interested in the trailing newlines + parts := strings.Split(strings.TrimRight(hello, "\r\n"), "\t") + + if len(parts) != 2 || parts[0] != "HELO" { + return errors.New("Bad hello from client") + } + + version, err := strconv.Atoi(parts[1]) + if version < 1 || version > 3 || err != nil { + return errors.New("Unknown protocol version requested") + } + + _, err = b.io.WriteString(fmt.Sprintf("OK\t%s\n", b.Banner)) + if err == nil { + err = b.io.Flush() + } + + if err == nil { + b.ProtocolVersion = version + } + return err +} + +func (b *Backend) handleQ(data string, callback Callback) ([]*Response, error) { + query := Query{ProtocolVersion: b.ProtocolVersion} + + err := query.fromData(data) + if err != nil { + return nil, err + } + + return callback(b, &query) +} + +// TODO +func (b *Backend) handleAXFR() ([]*Response, error) { + return nil, errors.New("AXFR requests not supported") +} + +// Reads lines in a loop, processing them by executing the provided callback +// and writing appropriate output in response, sequentially, until we hit an +// error or our IO hits EOF +func (b *Backend) Run(callback Callback) error { + responses := make([]*Response, 0) + + for { + line, err := b.io.ReadString('\n') + if err != nil { + if err == io.EOF { + return nil + } + return err + } + parts := strings.SplitN(strings.TrimRight(line, "\n"), "\t", 2) + if len(parts) == 2 { + + } + + switch parts[0] { + case "Q": + responses, err = b.handleQ(parts[1], callback) + case "PING": + responses, err = nil, nil // We just need to return END + case "AXFR": + responses, err = b.handleAXFR() + default: + responses, err = nil, errors.New("Bad command") + } + + if err != nil { + // avoid protocol errors + clean := strings.Replace(err.Error(), "\n", " ", -1) + msg := fmt.Sprintf("LOG\tError handling line: %s\nFAIL\n", clean) + _, err := b.io.WriteString(msg) + if err != nil { + return fmt.Errorf("%s while writing FAIL response", err) + } + // + err = b.io.Flush() + if err != nil { + return fmt.Errorf("%s while flushing FAIL response", err) + } + continue + } + + // DATA (if there are any records to return) + for _, response := range responses { + // Always output a line of the right protocol version + // TODO: panic if it's set to a wrong non-zero value? + response.ProtocolVersion = b.ProtocolVersion + data, err := response.String() + if err != nil { + data = "LOG\tError serialising response: " + err.Error() + "\n" + } + _, err = b.io.WriteString(data) + if err != nil { + return fmt.Errorf("%s while writing DATA response", err) + } + } + + // END + _, err = b.io.WriteString("END\n") + if err == nil { + err = b.io.Flush() + } + + if err != nil { + return fmt.Errorf("%s while writing END", err) + } + } + + // We should never hit this at the moment. + // TODO: graceful signal handling - intercept and ensure current query + // completes before breaking the above loop and returning nil here + return nil +} diff --git a/pipe/backend/backend_test.go b/pipe/backend/backend_test.go new file mode 100644 index 0000000..75e99bc --- /dev/null +++ b/pipe/backend/backend_test.go @@ -0,0 +1,169 @@ +package backend_test + +import ( + h "../test_helpers" + "bytes" + "errors" + "fmt" + . "github.com/BytemarkHosting/go-pdns/pipe/backend" + "strings" + "testing" +) + +// Test serializing Query & Response instances - we use them in the tests +func TestQueryStringV1(t *testing.T) { + h.AssertEqualString( + t, "Q\texample.com\tIN\tANY\t-1\t127.0.0.2\n", + h.FakeQueryString(t, 1), "V1 query serialisation problem", + ) +} + +func TestQueryStringV2(t *testing.T) { + h.AssertEqualString( + t, "Q\texample.com\tIN\tANY\t-1\t127.0.0.2\t127.0.0.1\n", + h.FakeQueryString(t, 2), "V2 query serialisation problem", + ) +} + +func TestQueryStringV3(t *testing.T) { + h.AssertEqualString( + t, "Q\texample.com\tIN\tANY\t-1\t127.0.0.2\t127.0.0.1\t127.0.0.3\n", + h.FakeQueryString(t, 3), "V3 query serialisation problem", + ) +} + +// Ensure we serialize Response instances correctly - we use them in the tests +func TestResponseStringV1andV2(t *testing.T) { + exemplar := "DATA\texample.com\tIN\tANY\t3600\t-1\tfoo\n" + + h.AssertEqualString(t, exemplar, h.FakeResponseString(t, 1), "V1 response serialisation problem") + h.AssertEqualString(t, exemplar, h.FakeResponseString(t, 2), "V2 response serialisation problem") +} + +func TestResponseStringV3(t *testing.T) { + h.AssertEqualString( + t, "DATA\t24\tauth\texample.com\tIN\tANY\t3600\t-1\tfoo\n", + h.FakeResponseString(t, 3), "V3 response serialisation problem", + ) +} + +func BuildAndNegotiate(t *testing.T, protoVersion int) (*Backend, *bytes.Buffer, *bytes.Buffer) { + r := bytes.NewBufferString(fmt.Sprintf("HELO\t%d\n", protoVersion)) + w := &bytes.Buffer{} + b := New(r, w, "Testing Backend") + + h.RefuteError(t, b.Negotiate(), "Negotiation failed") + h.AssertEqualInt(t, protoVersion, b.ProtocolVersion, "Bad protocol version") + h.AssertEqualString(t, "OK\tTesting Backend\n", w.String(), "Bad response to HELO") + w.Reset() + + return b, r, w +} + +func AssertRun(t *testing.T, b *Backend, f Callback) { + err := b.Run(f) + h.RefuteError(t, err, "Running backend") +} + +func AssertProtocolVersionNegotiation(t *testing.T, protoVersion int) { + b, r, w := BuildAndNegotiate(t, protoVersion) + r.WriteString(h.FakeQueryString(t, protoVersion)) + + // We also test that the negotiated version can be handled + AssertRun(t, b, h.EmptyDispatch) + h.AssertEqualString(t, "END\n", w.String(), "Unexpected response") + w.Reset() + + exp := fmt.Sprintf( + "LOG\tError handling line: v%d query should have %d data parts\nFAIL\n", + protoVersion, protoVersion+4, + ) + + // Test a short Q + r.WriteString("Q\tfoo\n") + AssertRun(t, b, h.EmptyDispatch) + h.AssertEqualString(t, exp, w.String(), "Unexpected response") + w.Reset() + + // test a long Q + long := strings.TrimRight(h.FakeQueryString(t, protoVersion), "\n") + "\tfoo\n" + r.WriteString(long) + AssertRun(t, b, h.EmptyDispatch) + h.AssertEqualString(t, exp, w.String(), "Unexpected response") +} + +func TestNegotiatedVersion1(t *testing.T) { + AssertProtocolVersionNegotiation(t, 1) +} +func TestNegotiatedVersion2(t *testing.T) { + AssertProtocolVersionNegotiation(t, 2) +} + +func TestNegotiatedVersion3(t *testing.T) { + AssertProtocolVersionNegotiation(t, 3) +} + +func TestQueriesArePassedToDispatcher(t *testing.T) { + b, r, w := BuildAndNegotiate(t, 3) + var outQ *Query + runs := 0 + + expected := h.FakeQueryString(t, 3) + r.WriteString(expected) + + AssertRun(t, b, func(b *Backend, q *Query) ([]*Response, error) { + runs = runs + 1 + outQ = q + return nil, nil + }) + h.AssertEqualInt(t, 1, runs, "Exactly one dispatch callback expected") + + txt, _ := outQ.String() + h.AssertEqualString(t, expected, txt, "Wrong query dispatched") + h.AssertEqualString(t, "END\n", w.String(), "Unexpected response") +} + +func TestResponsesFromDispatcherArePassedToAsker(t *testing.T) { + b, r, w := BuildAndNegotiate(t, 3) + r.WriteString(h.FakeQueryString(t, 3)) + + fr := h.FakeResponse(3) + AssertRun(t, b, func(b *Backend, q *Query) ([]*Response, error) { + return []*Response{fr, fr}, nil + }) + + exp := fmt.Sprintf("%s%sEND\n", h.FakeResponseString(t, 3), h.FakeResponseString(t, 3)) + h.AssertEqualString(t, exp, w.String(), "Bad response") +} + +func TestErrorFromDispatcherSuppressesResponses(t *testing.T) { + b, r, w := BuildAndNegotiate(t, 3) + r.WriteString(h.FakeQueryString(t, 3)) + + fr := h.FakeResponse(3) + AssertRun(t, b, func(b *Backend, q *Query) ([]*Response, error) { + return []*Response{fr, fr}, errors.New("Test\nerror") + }) + h.AssertEqualString(t, "LOG\tError handling line: Test error\nFAIL\n", w.String(), "Bad response") +} + +func TestHandlesPing(t *testing.T) { + b, r, w := BuildAndNegotiate(t, 3) + r.WriteString("PING\n") + AssertRun(t, b, h.EmptyDispatch) + h.AssertEqualString(t, "END\n", w.String(), "Bad response") +} + +func TestAXFRIsTODO(t *testing.T) { + b, r, w := BuildAndNegotiate(t, 3) + r.WriteString("AXFR\n") + AssertRun(t, b, h.EmptyDispatch) + h.AssertEqualString(t, "LOG\tError handling line: AXFR requests not supported\nFAIL\n", w.String(), "Bad response") +} + +func TestUnknownCommand(t *testing.T) { + b, r, w := BuildAndNegotiate(t, 3) + r.WriteString("GOGOGO\n") + AssertRun(t, b, h.EmptyDispatch) + h.AssertEqualString(t, "LOG\tError handling line: Bad command\nFAIL\n", w.String(), "Bad response") +} diff --git a/pipe/backend/query.go b/pipe/backend/query.go new file mode 100644 index 0000000..115f06a --- /dev/null +++ b/pipe/backend/query.go @@ -0,0 +1,85 @@ +package backend + +import ( + "errors" + "fmt" + "strings" +) + +// Represents a query received by the backend. Certain fields may be blank, +// depending on negotiated protocol version - which is stored in ProtocolVersion +// +// QName, QClass, QType, Id and RemoteIpAddress are present in all versions +// LocalIpAddress was added in version 2 +// EdnsSubnetAddress was added in version 3 +type Query struct { + ProtocolVersion int + QName string + QClass string + QType string + Id string + RemoteIpAddress string + LocalIpAddress string + EdnsSubnetAddress string +} + +func (q *Query) fromData(data string) (err error) { + parts := strings.Split(data, "\t") + + // ugh + switch q.ProtocolVersion { + case 1: + if len(parts) != 5 { + return errors.New("v1 query should have 5 data parts") + } + case 2: + if len(parts) != 6 { + return errors.New("v2 query should have 6 data parts") + } + q.LocalIpAddress = parts[5] + case 3: + if len(parts) != 7 { + return errors.New("v3 query should have 7 data parts") + } + q.LocalIpAddress = parts[5] + q.EdnsSubnetAddress = parts[6] + default: + return errors.New("Unknown protocol version in query") + } + + // common + q.QName = parts[0] + q.QClass = parts[1] + q.QType = parts[2] + q.Id = parts[3] + q.RemoteIpAddress = parts[4] + + return nil +} + +func (q *Query) String() (string, error) { + if q.ProtocolVersion < 1 || q.ProtocolVersion > 3 { + return "", errors.New("Unknown protocol version in query") + } + switch q.ProtocolVersion { + case 1: + return fmt.Sprintf( + "Q\t%s\t%s\t%s\t%s\t%s\n", + q.QName, q.QClass, q.QType, q.Id, q.RemoteIpAddress, + ), nil + case 2: + return fmt.Sprintf( + "Q\t%s\t%s\t%s\t%s\t%s\t%s\n", + q.QName, q.QClass, q.QType, q.Id, q.RemoteIpAddress, + q.LocalIpAddress, + ), nil + case 3: + return fmt.Sprintf( + "Q\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + q.QName, q.QClass, q.QType, q.Id, q.RemoteIpAddress, + q.LocalIpAddress, q.EdnsSubnetAddress, + ), nil + } + + return "", errors.New("Unknown protocol version in query") +} diff --git a/pipe/backend/response.go b/pipe/backend/response.go new file mode 100644 index 0000000..c3cc030 --- /dev/null +++ b/pipe/backend/response.go @@ -0,0 +1,41 @@ +package backend + +import ( + "errors" + "fmt" +) + +// A response to be sent back in answer to a query. Again, some fields may be +// blank, depending on protocol version. +// +// QName, QClass, QType, TTL, Id and Content are present in all versions +// No additions in version 2 +// ScopeBits and Auth were added in version 3 +type Response struct { + ProtocolVersion int + ScopeBits string + Auth string + QName string + QClass string + QType string + TTL string + Id string + Content string +} + +// Gives the response in a serialized form suitable for squirting on the wire +func (r *Response) String() (string, error) { + switch r.ProtocolVersion { + case 1, 2: + return fmt.Sprintf( + "DATA\t%s\t%s\t%s\t%s\t%s\t%s\n", + r.QName, r.QClass, r.QType, r.TTL, r.Id, r.Content, + ), nil + case 3: + return fmt.Sprintf( + "DATA\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n", + r.ScopeBits, r.Auth, r.QName, r.QClass, r.QType, r.TTL, r.Id, r.Content, + ), nil + } + return "", errors.New("Unknown protocol version in response") +} diff --git a/pipe/dsl/context.go b/pipe/dsl/context.go new file mode 100644 index 0000000..82795e9 --- /dev/null +++ b/pipe/dsl/context.go @@ -0,0 +1,59 @@ +package dsl + +import ( + "github.com/BytemarkHosting/go-pdns/pipe/backend" + "strconv" +) + +// Callbacks are run with a context instance, which allows them to accumulate +// answers while maintaining a short type signature. It will also make +// concurrent callbacks easier, when we handle that, but for now one context +// is maintained across all callbacks for a particular query +type Context struct { + // Replies that don't specify a TTL will be given this instead. + DefaultTTL int + + // The query that triggered this callback run. Note that its QType + // member may be "ANY" + Query *backend.Query + + // QType this callback is being run as. Matches the qtype field given + // with the callback at the time DSL.Register was called + QType string + + // The callback is registered with a regexp; if that regexp contains + // any match groups, then the matched text is placed here. + Matches []string + + // Set this if an error has been encountered; no more callbacks will be + // run, and the error text (only) will be reported to the backend. + Error error + + // Answers to be sent to the backend are stored here. Context.Reply() + // calls, etc, generate answers and put them here, for instance. + // If multiple callbacks are being run, then later callbacks will be + // able to see the answers earlier ones generated (for now) + Answers []*backend.Response +} + +// Add an answer, using default QName and TTL for the query +func (c *Context) Reply(content string) { + c.ReplyExtra(c.Query.QName, content, c.DefaultTTL) +} + +// Add an answer, using the default QName but specifying a particular TTL +func (c *Context) ReplyTTL(content string, ttl int) { + c.ReplyExtra(c.Query.QName, content, ttl) +} + +// Add an answer, specifying both QName and TTL. +func (c *Context) ReplyExtra(qname, content string, ttl int) { + c.Answers = append(c.Answers, &backend.Response{ + QName: qname, + QClass: c.Query.QClass, + QType: c.QType, // q.Query.QType may == "ANY" + Id: c.Query.Id, + Content: content, + TTL: strconv.Itoa(ttl), + }) +} diff --git a/pipe/dsl/dsl.go b/pipe/dsl/dsl.go new file mode 100644 index 0000000..b3ea0e8 --- /dev/null +++ b/pipe/dsl/dsl.go @@ -0,0 +1,223 @@ +// Copyright 2015 Bytemark Computer Consulting Ltd. All rights reserved +// Licensed under the GNU General Public License, version 2. See the LICENSE +// file for more details + +// Simple DSL for pipebackend. Usage: +// +// // Create new handle. You have to specify a default TTL here. +// x := dsl.New() +// root := regexp.QuoteMeta("example.com") +// +// // most zones need SOA + NS records +// x.SOA(root, func(c *dsl.Context) { +// c.Reply("ns1.example.com hostmaster.example.com 1 3600 1800 86400 3600") +// }) +// +// // Zones need NS records too. All replies will be returned +// x.NS(root, func(c *dsl.Context) { +// c.Reply("ns1.example.com") +// c.Reply("ns2.example.com") +// c.Reply("ns3.example.com") +// }) +// +// // You don't have to use anonymous functions, of course +// func answer(c *dsl.Context) { +// switch c.Query.QType { +// case "A" : c.Reply("169.254.0.1") +// case "AAAA": c.Reply("fe80::1" ) +// } +// } +// x.A(root, answer) +// x.AAAA(root, answer) +// +// // Setting c.Error at any point will suppress *all* replies from being +// // sent back. Instead, a FAIL response with the c.Error.Error() as the +// // content is returned to powerdns +// x.SSHFP(root, func(c *dsl.Context) { +// c.Reply("1 1 f1d2d2f924e986ac86fdf7b36c94bcdf32beec15") +// c.Error = errors.New("Don't use SSHFP on unsigned zones") +// c.Reply("1 2 e242ed3bffccdf271b7fbaf34ed72d089537b42f") +// }) +// +// // You can do anything in a callback, but be aware that powerdns has a +// // time limit on responses and there is no request concurrency within a +// // single pipe connection. pdns achieves concurrency through multiple +// // backend connections instead +// c := make(chan string) +// x.MX(root, func(c *dsl.Context) { +// c.Reply(<-c) +// }) +// +// // If your regexp includes capture groups, they are quoted back to you. +// // Here's a simple DNS echo server. Note the use of ReplyExtra to allow +// // a non-default TTL to be set. +// // +// // Don't forget: DNS is supposed to be case-insensitive. Be careful. +// c.TXT(`(.*)\.` + root, func(c *dsl.Context) { +// c.ReplyTTL(c.Query.QName, c.Matches[0], 0) +// }) +// +// // Dispatch is up to you. It will probably look like this, but you +// // might want to add logging around the request or something more +// // complicated (different DSL instance depending on backend version?) +// func doit(b *backend.Backend, q *backend.Query) ([]*backend.Response, error) { +// if q.QClass == "IN" { +// return x.Lookup(q) +// } +// return nil, errors.New("Only IN QClass is supported") +// } +// +// pipe := backend.New( r, w, "Example backend" ) +// err1 := pipe.Negotiate() // do check for errors +// err2 := pipe.Run(doit) +// +// +// +package dsl + +import ( + "github.com/BytemarkHosting/go-pdns/pipe/backend" + "regexp" +) + +// Instances of this struct are used to hold onto registered callbacks, etc. +type DSL struct { + callbacks map[string][]callbackNode + qtypeSort []string + defaultTTL int + + beforeCallback Callback +} + +// Get a new builder with a default TTL of one hour +func New() *DSL { + return NewWithTTL(3600) +} + +// Get a new builder, specifying a default TTL explicitly +func NewWithTTL(ttl int) *DSL { + return &DSL{ + callbacks: make(map[string][]callbackNode), + qtypeSort: make([]string, 0), + defaultTTL: ttl, + } +} + +// Callbacks are registered against the DSL instance and run against incoming +// queries if the regexp they are registered with matches the QName of the query +type Callback func(c *Context) + +type callbackNode struct { + matcher *regexp.Regexp + fn Callback +} + +// Register a callback to run before every request. Set c.Error to halt +// processing, or mutate the context however you like. +func (d *DSL) Before(f Callback) { + d.beforeCallback = f +} + +// Register a callback to be run whenever a query with a QName matching the +// regular expression comes in. The regex is provided as a string (matcher) +// to keep ordinary invocations short; it's compiled immediately with +// regexp.MustCompile. Don't forget to anchor your regexes! +// +// If match groups are included in the regex, then any matched text is placed in +// the Context the callback receives. +// +// Callbacks are run with slightly obtuse ordering: all callbacks of a qtype +// are run in the order they were registered. We iterate the list of qtypes +// in the order that a callback with a matching qtype was *first* registered. +// If your pdns server has the "noshuffle" configuration directive, the order +// will be reflected in the responses returned by it; future concurrent DSL +// should maintain this ordering. +func (d *DSL) Register(qtype string, re *regexp.Regexp, f Callback) { + + // Maintain our obtuse sense of order + alreadyIn := false + for _, prospect := range d.qtypeSort { + if prospect == qtype { + alreadyIn = true + break + } + } + if !alreadyIn { + d.qtypeSort = append(d.qtypeSort, qtype) + } + + node := callbackNode{matcher: re, fn: f} + d.callbacks[qtype] = append(d.callbacks[qtype], node) +} + +// Once we're concurrent, this method will create the context and return it +func (d *DSL) runNode(c *Context, node *callbackNode) { + matches := node.matcher.FindStringSubmatch(c.Query.QName) + + if matches != nil && len(matches) > 0 { + // Probably unnecessary, but ensure that the previous value of + // Matches is preserved. This could also be = nil + oldmatches := c.Matches + defer func(c *Context) { c.Matches = oldmatches }(c) + + // The first match is the whole thing, followed by the capture + // groups. We're only interested in the latter. + c.Matches = matches[1:] + + if d.beforeCallback != nil { + d.beforeCallback(c) + } + + if c.Error == nil { + node.fn(c) + } + } +} + +// Run all registered callbacks against the query. If any callbacks report an +// error, we halt and return the error only (partially constructed responses are +// discarded). +// +// For now, callbacks are run sequentially, rather than in parallel. There could +// be a speedup to running each callback in its own goroutine. Currently, all +// callbacks share the same context instance; we'd have to change that if we +// ran them in parallel. +func (d *DSL) Lookup(q *backend.Query) ([]*backend.Response, error) { + c := Context{ + DefaultTTL: d.defaultTTL, + Query: q, + Answers: make([]*backend.Response, 0), + Error: nil, + } + + var runOn []string + if q.QType == "ANY" { + runOn = d.qtypeSort + } else { + runOn = []string{q.QType} + } + + for _, qtype := range runOn { + c.QType = qtype + for _, node := range d.callbacks[qtype] { + d.runNode(&c, &node) + if c.Error != nil { + return nil, c.Error + } + } + } + + return c.Answers, nil +} + +// Reports the registered callbacks, in order. Handy for testing or status. +func (d *DSL) String() string { + out := "" + for _, qtype := range d.qtypeSort { + out = out + qtype + "\t:" + for _, node := range d.callbacks[qtype] { + out = out + "\t" + node.matcher.String() + "\n" + } + } + return out +} diff --git a/pipe/dsl/dsl_helpers.go b/pipe/dsl/dsl_helpers.go new file mode 100644 index 0000000..ea0cbc8 --- /dev/null +++ b/pipe/dsl/dsl_helpers.go @@ -0,0 +1,1163 @@ +// AUTOGENERATED helper methods for IANA-registered RRTYPES. Do not edit. +// See generate_dsl_helpers.sh for details +package dsl + +import ( + "fmt" + "regexp" +) + +var RRTypes = []string{ + "A", + "AAAA", + "AFSDB", + "APL", + "ATMA", + "AXFR", + "CAA", + "CDNSKEY", + "CDS", + "CERT", + "CNAME", + "DHCID", + "DLV", + "DNAME", + "DNSKEY", + "DS", + "EID", + "GID", + "GPOS", + "HINFO", + "HIP", + "IPSECKEY", + "ISDN", + "IXFR", + "KEY", + "KX", + "LOC", + "LP", + "MAILA", + "MAILB", + "MB", + "MD", + "MF", + "MG", + "MINFO", + "MR", + "MX", + "NAPTR", + "NID", + "NIMLOC", + "NINFO", + "NS", + "NSAP", + "NSEC", + "NULL", + "NXT", + "OPENPGPKEY", + "OPT", + "PTR", + "PX", + "RKEY", + "RP", + "RRSIG", + "RT", + "SIG", + "SINK", + "SOA", + "SPF", + "SRV", + "SSHFP", + "TA", + "TALINK", + "TKEY", + "TLSA", + "TSIG", + "TXT", + "TYPE", + "UID", + "UINFO", + "UNSPEC", + "URI", + "WKS", +} + +// Helper function to register a callback for A queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "A" string directly. +func (d *DSL) A(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("A", re, f) +} + +// Helper function to register a callback for AAAA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "AAAA" string directly. +func (d *DSL) AAAA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("AAAA", re, f) +} + +// Helper function to register a callback for AFSDB queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "AFSDB" string directly. +func (d *DSL) AFSDB(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("AFSDB", re, f) +} + +// Helper function to register a callback for APL queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "APL" string directly. +func (d *DSL) APL(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("APL", re, f) +} + +// Helper function to register a callback for ATMA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "ATMA" string directly. +func (d *DSL) ATMA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("ATMA", re, f) +} + +// Helper function to register a callback for AXFR queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "AXFR" string directly. +func (d *DSL) AXFR(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("AXFR", re, f) +} + +// Helper function to register a callback for CAA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "CAA" string directly. +func (d *DSL) CAA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("CAA", re, f) +} + +// Helper function to register a callback for CDNSKEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "CDNSKEY" string directly. +func (d *DSL) CDNSKEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("CDNSKEY", re, f) +} + +// Helper function to register a callback for CDS queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "CDS" string directly. +func (d *DSL) CDS(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("CDS", re, f) +} + +// Helper function to register a callback for CERT queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "CERT" string directly. +func (d *DSL) CERT(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("CERT", re, f) +} + +// Helper function to register a callback for CNAME queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "CNAME" string directly. +func (d *DSL) CNAME(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("CNAME", re, f) +} + +// Helper function to register a callback for DHCID queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "DHCID" string directly. +func (d *DSL) DHCID(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("DHCID", re, f) +} + +// Helper function to register a callback for DLV queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "DLV" string directly. +func (d *DSL) DLV(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("DLV", re, f) +} + +// Helper function to register a callback for DNAME queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "DNAME" string directly. +func (d *DSL) DNAME(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("DNAME", re, f) +} + +// Helper function to register a callback for DNSKEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "DNSKEY" string directly. +func (d *DSL) DNSKEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("DNSKEY", re, f) +} + +// Helper function to register a callback for DS queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "DS" string directly. +func (d *DSL) DS(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("DS", re, f) +} + +// Helper function to register a callback for EID queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "EID" string directly. +func (d *DSL) EID(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("EID", re, f) +} + +// Helper function to register a callback for GID queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "GID" string directly. +func (d *DSL) GID(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("GID", re, f) +} + +// Helper function to register a callback for GPOS queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "GPOS" string directly. +func (d *DSL) GPOS(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("GPOS", re, f) +} + +// Helper function to register a callback for HINFO queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "HINFO" string directly. +func (d *DSL) HINFO(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("HINFO", re, f) +} + +// Helper function to register a callback for HIP queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "HIP" string directly. +func (d *DSL) HIP(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("HIP", re, f) +} + +// Helper function to register a callback for IPSECKEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "IPSECKEY" string directly. +func (d *DSL) IPSECKEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("IPSECKEY", re, f) +} + +// Helper function to register a callback for ISDN queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "ISDN" string directly. +func (d *DSL) ISDN(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("ISDN", re, f) +} + +// Helper function to register a callback for IXFR queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "IXFR" string directly. +func (d *DSL) IXFR(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("IXFR", re, f) +} + +// Helper function to register a callback for KEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "KEY" string directly. +func (d *DSL) KEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("KEY", re, f) +} + +// Helper function to register a callback for KX queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "KX" string directly. +func (d *DSL) KX(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("KX", re, f) +} + +// Helper function to register a callback for LOC queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "LOC" string directly. +func (d *DSL) LOC(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("LOC", re, f) +} + +// Helper function to register a callback for LP queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "LP" string directly. +func (d *DSL) LP(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("LP", re, f) +} + +// Helper function to register a callback for MAILA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MAILA" string directly. +func (d *DSL) MAILA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MAILA", re, f) +} + +// Helper function to register a callback for MAILB queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MAILB" string directly. +func (d *DSL) MAILB(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MAILB", re, f) +} + +// Helper function to register a callback for MB queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MB" string directly. +func (d *DSL) MB(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MB", re, f) +} + +// Helper function to register a callback for MD queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MD" string directly. +func (d *DSL) MD(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MD", re, f) +} + +// Helper function to register a callback for MF queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MF" string directly. +func (d *DSL) MF(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MF", re, f) +} + +// Helper function to register a callback for MG queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MG" string directly. +func (d *DSL) MG(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MG", re, f) +} + +// Helper function to register a callback for MINFO queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MINFO" string directly. +func (d *DSL) MINFO(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MINFO", re, f) +} + +// Helper function to register a callback for MR queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MR" string directly. +func (d *DSL) MR(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MR", re, f) +} + +// Helper function to register a callback for MX queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "MX" string directly. +func (d *DSL) MX(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("MX", re, f) +} + +// Helper function to register a callback for NAPTR queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NAPTR" string directly. +func (d *DSL) NAPTR(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NAPTR", re, f) +} + +// Helper function to register a callback for NID queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NID" string directly. +func (d *DSL) NID(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NID", re, f) +} + +// Helper function to register a callback for NIMLOC queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NIMLOC" string directly. +func (d *DSL) NIMLOC(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NIMLOC", re, f) +} + +// Helper function to register a callback for NINFO queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NINFO" string directly. +func (d *DSL) NINFO(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NINFO", re, f) +} + +// Helper function to register a callback for NS queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NS" string directly. +func (d *DSL) NS(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NS", re, f) +} + +// Helper function to register a callback for NSAP queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NSAP" string directly. +func (d *DSL) NSAP(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NSAP", re, f) +} + +// Helper function to register a callback for NSEC queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NSEC" string directly. +func (d *DSL) NSEC(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NSEC", re, f) +} + +// Helper function to register a callback for NULL queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NULL" string directly. +func (d *DSL) NULL(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NULL", re, f) +} + +// Helper function to register a callback for NXT queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "NXT" string directly. +func (d *DSL) NXT(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("NXT", re, f) +} + +// Helper function to register a callback for OPENPGPKEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "OPENPGPKEY" string directly. +func (d *DSL) OPENPGPKEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("OPENPGPKEY", re, f) +} + +// Helper function to register a callback for OPT queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "OPT" string directly. +func (d *DSL) OPT(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("OPT", re, f) +} + +// Helper function to register a callback for PTR queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "PTR" string directly. +func (d *DSL) PTR(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("PTR", re, f) +} + +// Helper function to register a callback for PX queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "PX" string directly. +func (d *DSL) PX(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("PX", re, f) +} + +// Helper function to register a callback for RKEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "RKEY" string directly. +func (d *DSL) RKEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("RKEY", re, f) +} + +// Helper function to register a callback for RP queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "RP" string directly. +func (d *DSL) RP(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("RP", re, f) +} + +// Helper function to register a callback for RRSIG queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "RRSIG" string directly. +func (d *DSL) RRSIG(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("RRSIG", re, f) +} + +// Helper function to register a callback for RT queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "RT" string directly. +func (d *DSL) RT(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("RT", re, f) +} + +// Helper function to register a callback for SIG queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "SIG" string directly. +func (d *DSL) SIG(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("SIG", re, f) +} + +// Helper function to register a callback for SINK queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "SINK" string directly. +func (d *DSL) SINK(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("SINK", re, f) +} + +// Helper function to register a callback for SOA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "SOA" string directly. +func (d *DSL) SOA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("SOA", re, f) +} + +// Helper function to register a callback for SPF queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "SPF" string directly. +func (d *DSL) SPF(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("SPF", re, f) +} + +// Helper function to register a callback for SRV queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "SRV" string directly. +func (d *DSL) SRV(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("SRV", re, f) +} + +// Helper function to register a callback for SSHFP queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "SSHFP" string directly. +func (d *DSL) SSHFP(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("SSHFP", re, f) +} + +// Helper function to register a callback for TA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TA" string directly. +func (d *DSL) TA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TA", re, f) +} + +// Helper function to register a callback for TALINK queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TALINK" string directly. +func (d *DSL) TALINK(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TALINK", re, f) +} + +// Helper function to register a callback for TKEY queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TKEY" string directly. +func (d *DSL) TKEY(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TKEY", re, f) +} + +// Helper function to register a callback for TLSA queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TLSA" string directly. +func (d *DSL) TLSA(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TLSA", re, f) +} + +// Helper function to register a callback for TSIG queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TSIG" string directly. +func (d *DSL) TSIG(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TSIG", re, f) +} + +// Helper function to register a callback for TXT queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TXT" string directly. +func (d *DSL) TXT(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TXT", re, f) +} + +// Helper function to register a callback for TYPE queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "TYPE" string directly. +func (d *DSL) TYPE(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("TYPE", re, f) +} + +// Helper function to register a callback for UID queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "UID" string directly. +func (d *DSL) UID(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("UID", re, f) +} + +// Helper function to register a callback for UINFO queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "UINFO" string directly. +func (d *DSL) UINFO(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("UINFO", re, f) +} + +// Helper function to register a callback for UNSPEC queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "UNSPEC" string directly. +func (d *DSL) UNSPEC(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("UNSPEC", re, f) +} + +// Helper function to register a callback for URI queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "URI" string directly. +func (d *DSL) URI(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("URI", re, f) +} + +// Helper function to register a callback for WKS queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string("^" at start) +// * The case-insensitivity option is added "(?i)" +// * The regexp is anchored to the end of the match string ("$" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the "WKS" string directly. +func (d *DSL) WKS(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf("^(?i)%s$", matcher)) + d.Register("WKS", re, f) +} diff --git a/pipe/dsl/dsl_test.go b/pipe/dsl/dsl_test.go new file mode 100644 index 0000000..0116750 --- /dev/null +++ b/pipe/dsl/dsl_test.go @@ -0,0 +1,181 @@ +package dsl_test + +import ( + "errors" + "fmt" + "github.com/BytemarkHosting/go-pdns/pipe/backend" + . "github.com/BytemarkHosting/go-pdns/pipe/dsl" + h "github.com/BytemarkHosting/go-pdns/pipe/test_helpers" + "strings" + "testing" +) + +func ReplyHandler(x string) func(c *Context) { + return func(c *Context) { c.Reply(x) } +} + +func NullHandler(c *Context) {} + +var ErrorReplyError = errors.New("Foo") + +func ErrorReplyHandler(c *Context) { c.Error = ErrorReplyError } + +func SOAQuery() *backend.Query { + q := h.FakeQuery(3) + q.QName = "example.com" + q.QType = "SOA" + return q +} + +func AssertTableEntry(t *testing.T, d *DSL, qtype, matcher, msg string) { + table := qtype + "\t:\t" + matcher + "\n" + h.AssertEqualString(t, table, d.String(), msg) +} + +func AssertLookup(t *testing.T, d *DSL, q *backend.Query, n int, err error) []*backend.Response { + rsp, rspErr := d.Lookup(q) + if err == nil { + h.RefuteError(t, err, "Lookup shouldn't return error") + } else if rspErr == nil { + t.Logf("Expected error %s but no error was returned", err) + t.FailNow() + } else { + h.AssertEqualString(t, err.Error(), rspErr.Error(), "Expected error not returned") + } + + rspStrs := []string{} + for _, r := range rsp { + r.ProtocolVersion = 1 + str, err := r.String() + h.RefuteError(t, err, "sanity") + rspStrs = append(rspStrs, str) + } + + h.AssertEqualInt(t, n, len(rsp), fmt.Sprintf("One response expected, got:\n%s", strings.Join(rspStrs, ""))) + return rsp +} + +// Don't test all the autogenerated helpers explicitly, just a common one. +func TestAutogeneratedExampleRegistersCorrectCallbackWhenRun(t *testing.T) { + d := New() + d.SOA(`example\.com`, NullHandler) + AssertTableEntry(t, d, "SOA", `^(?i)example\.com$`, "SOA callback not registered") +} + +func TestDefaultTTLFromNewIsOneHour(t *testing.T) { + d := New() + d.SOA(`*`, ReplyHandler("Foo")) + rsp := AssertLookup(t, d, SOAQuery(), 1, nil) + h.AssertEqualString(t, "3600", rsp[0].TTL, "Default TTL not honoured") +} + +func TestAlternativeTTLCanBeSpecifiedUsingNewWithTTL(t *testing.T) { + d := NewWithTTL(86400) + d.SOA(`*`, ReplyHandler("Foo")) + rsp := AssertLookup(t, d, SOAQuery(), 1, nil) + h.AssertEqualString(t, "86400", rsp[0].TTL, "Custom TTL not honoured") +} + +func TestBeforeCallbackIsCalledIfSpecified(t *testing.T) { + d := New() + d.Before(ReplyHandler("Before")) + d.SOA(`*`, ReplyHandler("SOA 1")) + d.SOA(`*`, ReplyHandler("SOA 2")) + rsp := AssertLookup(t, d, SOAQuery(), 4, nil) + h.AssertEqualString(t, "Before", rsp[0].Content, "First Before not called") + h.AssertEqualString(t, "SOA 1", rsp[1].Content, "First SOA not called") + h.AssertEqualString(t, "Before", rsp[2].Content, "Second Before not called") + h.AssertEqualString(t, "SOA 2", rsp[3].Content, "Second SOA not called") +} + +func TestLookupCallbackOrderIsDefined(t *testing.T) { + d := New() + d.SOA(`*`, ReplyHandler("SOA 1")) + d.MX(`*`, ReplyHandler("MX 1")) + d.SOA(`*`, ReplyHandler("SOA 2")) + d.SOA(`*`, ReplyHandler("SOA 3")) + d.AAAA(`*`, ReplyHandler("AAAA 1")) + d.A(`*`, ReplyHandler("AAAA 2")) + rsp := AssertLookup(t, d, h.FakeQuery(1), 6, nil) + msg := "Order is wrong" + h.AssertEqualString(t, "SOA 1", rsp[0].Content, msg) + h.AssertEqualString(t, "SOA 2", rsp[1].Content, msg) + h.AssertEqualString(t, "SOA 3", rsp[2].Content, msg) + h.AssertEqualString(t, "MX 1", rsp[3].Content, msg) + h.AssertEqualString(t, "AAAA 1", rsp[4].Content, msg) + h.AssertEqualString(t, "AAAA 2", rsp[5].Content, msg) +} + +func TestOnlyMatchingCallbacksAreRun(t *testing.T) { + d := New() + d.SOA(`example\.com`, ReplyHandler("Good")) + d.SOA(`example\.org`, ReplyHandler("Bad")) + rsp := AssertLookup(t, d, SOAQuery(), 1, nil) + h.AssertEqualString(t, "Good", rsp[0].Content, "Wrong callback run") +} + +func TestOnlyCallbacksOfTheRightQTypeAreRun(t *testing.T) { + d := New() + d.SOA(`example\.com`, ReplyHandler("Good")) + d.NS(`example\.com`, ReplyHandler("Bad")) + rsp := AssertLookup(t, d, SOAQuery(), 1, nil) + h.AssertEqualString(t, "Good", rsp[0].Content, "Wrong callback run") +} + +func TestCaptureGroupsArePutIntoContextMatches(t *testing.T) { + d := New() + var captures []string + d.SOA(`([a-z]{3})\.([a-z]{3})\.([a-z]{3})\.example\.com`, func(c *Context) { + captures = c.Matches + c.Reply("OK") + }) + q := SOAQuery() + q.QName = "BAZ.bar.foo.example.com" + rsp := AssertLookup(t, d, q, 1, nil) + h.AssertEqualInt(t, 3, len(captures), "Wrong number of match groups returned") + h.AssertEqualString(t, "OK", rsp[0].Content, "Wrong callback run?") + // case-insensitive, so we don't mangle these + h.AssertEqualString(t, "BAZ", captures[0], "Part 1 not captured") + h.AssertEqualString(t, "bar", captures[1], "Part 2 not captured") + h.AssertEqualString(t, "foo", captures[2], "Part 3 not captured") +} + +func TestBeforeCanModifyCaptureGroups(t *testing.T) { + d := New() + d.Before(func(c *Context) { c.Matches = append(c.Matches, "modify-cg") }) + d.SOA(`*`, func(c *Context) { c.Reply(c.Matches[0]) }) + rsp := AssertLookup(t, d, SOAQuery(), 1, nil) + h.AssertEqualString(t, "modify-cg", rsp[0].Content, "Capture modification lost") +} + +func TestChangesToMatchesInOrdinaryCallbacksDoNotPersist(t *testing.T) { + d := New() + var ok bool + + d.SOA(`*`, func(c *Context) { c.Matches = append(c.Matches, "Bad") }) + d.SOA(`*`, func(c *Context) { ok = (len(c.Matches) == 0) }) + AssertLookup(t, d, SOAQuery(), 0, nil) + h.Assert(t, ok, "Change was persisted") +} + +func TestCallbackReturningErrorIsReported(t *testing.T) { + d := New() + d.SOA(`*`, ErrorReplyHandler) + AssertLookup(t, d, SOAQuery(), 0, ErrorReplyError) +} + +func TestCallbackReturningErrorBlanksAnswers(t *testing.T) { + d := New() + d.SOA(`*`, ReplyHandler("Foo")) + d.SOA(`*`, ErrorReplyHandler) + AssertLookup(t, d, SOAQuery(), 0, ErrorReplyError) +} + +func TestCallbackReturningErrorStopsLaterCallbacksFromRunning(t *testing.T) { + d := New() + ok := true + d.SOA(`*`, ErrorReplyHandler) + d.SOA(`*`, func(c *Context) { ok = false }) + AssertLookup(t, d, SOAQuery(), 0, ErrorReplyError) + h.Assert(t, ok, "Later callback was run") +} diff --git a/pipe/dsl/generate_dsl_helpers.sh b/pipe/dsl/generate_dsl_helpers.sh new file mode 100755 index 0000000..624a752 --- /dev/null +++ b/pipe/dsl/generate_dsl_helpers.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +# + +records=`curl -sf http://www.iana.org/assignments/dns-parameters/dns-parameters-4.csv | cut -f1 -d',' | sort | uniq | awk '/^[A-Z][A-Z]*$/ { print $0 }'` || exit 1 + +echo "// AUTOGENERATED helper methods for IANA-registered RRTYPES. Do not edit." +echo "// See generate_dsl_helpers.sh for details" +echo "package dsl" +echo " +import ( + \"fmt\" + \"regexp\" +) +" +# A list of RRTypes might come in handy, you never know +echo "var RRTypes = []string{" +for record in $records; do +echo " \"${record}\"," +done +echo "}" +for record in $records; do + +echo " +// Helper function to register a callback for ${record} queries. +// The matcher is given as a string, which is compiled to a regular expression +// (using regexp.MustCompile) with the following rules: +// +// * The regexp is anchored to the start of the match string(\"^\" at start) +// * The case-insensitivity option is added \"(?i)\" +// * The regexp is anchored to the end of the match string (\"$\" at end) +// +// If any of these options are unwelcome, you can use the DSL.Register and pass +// a regexp and the \"${record}\" string directly. +func (d *DSL) ${record}(matcher string, f Callback) { + re := regexp.MustCompile(fmt.Sprintf(\"^(?i)%s$\", matcher)) + d.Register(\"${record}\", re, f) +}" + +done + diff --git a/pipe/test_helpers/test_helpers.go b/pipe/test_helpers/test_helpers.go new file mode 100644 index 0000000..beeff84 --- /dev/null +++ b/pipe/test_helpers/test_helpers.go @@ -0,0 +1,98 @@ +package test_helpers + +import( + "fmt" + "github.com/BytemarkHosting/go-pdns/pipe/backend" + "testing" +) + +func EmptyDispatch(b *backend.Backend, q *backend.Query)([]*backend.Response,error) { + return nil, nil +} + + +func AssertEqualString(t *testing.T, a, b, msg string) { + if a != b { + t.Logf(fmt.Sprintf("%s:'\n%s\n'\nshould be the same as:'\n%s\n'", msg, a, b)) + t.FailNow() + } +} + +func AssertEqualInt(t *testing.T, a, b int, msg string) { + if a != b { + t.Logf(fmt.Sprintf("%s: %d should == %d", msg, a, b)) + t.FailNow() + } +} + +func RefuteError(t *testing.T, err error, msg string) { + if err != nil { + t.Logf("Error: %s was expected to be nil", msg, err) + t.FailNow() + } +} + +func Assert(t *testing.T, condition bool, msg string) { + if !condition { + t.Log(msg) + t.FailNow() + } +} + +func FakeQuery(protoVersion int) *backend.Query { + if protoVersion < 1 || protoVersion > 3 { + panic("Invalid protoVersion") + } + + q := backend.Query{ + ProtocolVersion: protoVersion, + QClass: "IN", + QType: "ANY", + QName: "example.com", + Id: "-1", + RemoteIpAddress: "127.0.0.2", + } + if protoVersion > 1 { + q.LocalIpAddress = "127.0.0.1" + } + if protoVersion > 2 { + q.EdnsSubnetAddress = "127.0.0.3" + } + + return &q +} + +func FakeResponse(protoVersion int) *backend.Response { + if protoVersion < 1 || protoVersion > 3 { + panic("Invalid protoVersion") + } + + r := backend.Response{ + ProtocolVersion: protoVersion, + QClass: "IN", + QType: "ANY", + QName: "example.com", + Id: "-1", + TTL: "3600", + Content: "foo", + } + if protoVersion > 2 { + r.ScopeBits = "24" + r.Auth = "auth" + } + + return &r +} + +func FakeQueryString(t *testing.T, protoVersion int) string { + str, err := FakeQuery(protoVersion).String() + RefuteError(t, err, "Failed to serialise test query") + return str +} + +func FakeResponseString(t *testing.T, protoVersion int) string { + str, err := FakeResponse(protoVersion).String() + RefuteError(t, err, "Failed to serialise test response") + return str +} +