├── LICENSE ├── cmd └── ndbquery │ ├── ndbquery.go │ └── readme.md ├── ndb.go ├── ndb_test.go ├── readme.md └── testndb ├── common └── local /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nick Owens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/ndbquery/ndbquery.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/mischief/ndb" 7 | "os" 8 | ) 9 | 10 | var ( 11 | ndbfile = flag.String("f", ndb.NdbLocal, "ndb file") 12 | ) 13 | 14 | func usage() { 15 | fmt.Fprintf(os.Stderr, "Usage: %s [-f ndbfile] attr val [rattr]\n", os.Args[0]) 16 | flag.PrintDefaults() 17 | } 18 | 19 | func main() { 20 | flag.Usage = usage 21 | flag.Parse() 22 | 23 | narg := flag.NArg() 24 | 25 | if narg < 2 || narg > 3 { 26 | usage() 27 | os.Exit(1) 28 | } 29 | 30 | db, err := ndb.Open(*ndbfile) 31 | 32 | if err != nil { 33 | fmt.Fprint(os.Stderr, err) 34 | os.Exit(1) 35 | } 36 | 37 | records := db.Search(flag.Arg(0), flag.Arg(1)) 38 | 39 | switch narg { 40 | case 2: 41 | // print all attributes 42 | for _, rec := range records { 43 | for _, tuple := range rec { 44 | fmt.Printf("%s=%s ", tuple.Attr, tuple.Val) 45 | } 46 | fmt.Print("\n") 47 | } 48 | 49 | case 3: 50 | // only print rattr 51 | for _, rec := range records { 52 | for _, tuple := range rec { 53 | if tuple.Attr == flag.Arg(2) { 54 | fmt.Printf("%s\n", tuple.Val) 55 | } 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /cmd/ndbquery/readme.md: -------------------------------------------------------------------------------- 1 | ndbquery: simple ndb reader 2 | ======== 3 | 4 | if /usr/local/plan9/ndb/root-servers contains a file with an ndb entry such as 5 | 6 | dom=A.ROOT-SERVERS.NET ip=198.41.0.4 7 | 8 | a call to ndbquery might go like so: 9 | 10 | $ ndbquery -f /usr/local/plan9/ndb/root-servers dom A.ROOT-SERVERS.NET ip 11 | 198.41.0.4 12 | 13 | -------------------------------------------------------------------------------- /ndb.go: -------------------------------------------------------------------------------- 1 | // Package ndb implements the Network Database described in 2 | // http://plan9.bell-labs.com/magic/man2html/6/ndb. 3 | package ndb 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "io/ioutil" 10 | "os" 11 | "strings" 12 | "time" 13 | "unicode" 14 | "unicode/utf8" 15 | ) 16 | 17 | const ( 18 | // Default NDB file 19 | NdbLocal = "/lib/ndb/local" 20 | ) 21 | 22 | // A single database attribute=value tuple. 23 | // The value may be empty. 24 | type Tuple struct { 25 | Attr, Val string 26 | } 27 | 28 | // A NDB record, which may contain multiple tuples, 29 | // and may span multiple lines in the file. 30 | type Record []Tuple 31 | 32 | // RecordSet is a related group of records, from a single entry in ndb. 33 | type RecordSet []Record 34 | 35 | // Search a RecordSet for a given attribute and return the value. 36 | // Returns "" if not present. 37 | func (r RecordSet) Search(attr string) string { 38 | for _, rec := range r { 39 | for _, tuple := range rec { 40 | if tuple.Attr == attr { 41 | return tuple.Val 42 | } 43 | } 44 | } 45 | 46 | return "" 47 | } 48 | 49 | // Ndb possibly comprised of multiple files. 50 | type Ndb struct { 51 | filename string // NDB file name 52 | data *bytes.Reader // Raw data 53 | mtime time.Time // Last modified time 54 | records RecordSet // NDB Records 55 | next *Ndb // Next in linked list 56 | } 57 | 58 | // Open an NDB database file. 59 | func Open(fname string) (*Ndb, error) { 60 | var db, first, last *Ndb 61 | var err error 62 | 63 | if fname == "" { 64 | fname = NdbLocal 65 | } 66 | db, err = openone(fname) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | first = db 72 | last = db 73 | 74 | // open other db files 75 | if dbrec := db.Search("database", ""); dbrec != nil { 76 | 77 | for _, files := range dbrec[0] { 78 | if files.Attr == "file" { 79 | if files.Val == fname { 80 | if first.next == nil { 81 | continue 82 | } 83 | if first.filename == fname { 84 | db = first 85 | first = first.next 86 | last.next = db 87 | last = db 88 | } 89 | continue 90 | } 91 | if db, err = openone(files.Val); err != nil { 92 | return nil, err 93 | } 94 | last.next = db 95 | last = db 96 | } 97 | } 98 | } 99 | 100 | return first, nil 101 | } 102 | 103 | // Open just one NDB file 104 | func openone(fname string) (*Ndb, error) { 105 | db := &Ndb{filename: fname} 106 | 107 | // open file 108 | f, err := os.Open(db.filename) 109 | 110 | if err != nil { 111 | return nil, fmt.Errorf("open: %s", err) 112 | } 113 | 114 | defer f.Close() 115 | 116 | // read mtime 117 | if fstat, err := f.Stat(); err != nil { 118 | return nil, fmt.Errorf("open: %s", err) 119 | } else { 120 | db.mtime = fstat.ModTime() 121 | } 122 | 123 | // read all data 124 | if data, err := ioutil.ReadAll(f); err != nil { 125 | return nil, fmt.Errorf("open: %s", err) 126 | } else { 127 | db.data = bytes.NewReader(data) 128 | } 129 | 130 | // parse records 131 | if db.records, err = parserec(db); err != nil { 132 | return nil, fmt.Errorf("open: %s", err) 133 | } 134 | 135 | return db, nil 136 | } 137 | 138 | // Reopen NDB file. 139 | func (n *Ndb) Reopen() error { 140 | for db := n; db != nil; db = db.next { 141 | if newdb, err := openone(db.filename); err != nil { 142 | return err 143 | } else { 144 | db.data = newdb.data 145 | db.mtime = newdb.mtime 146 | db.records = newdb.records 147 | } 148 | } 149 | 150 | return nil 151 | } 152 | 153 | // Check if any db files changed. 154 | func (n *Ndb) Changed() (bool, error) { 155 | for db := n; db != nil; db = db.next { 156 | fi, err := os.Stat(db.filename) 157 | if err != nil { 158 | return false, err 159 | } 160 | 161 | if db.mtime != fi.ModTime() { 162 | return true, nil 163 | } 164 | } 165 | 166 | return false, nil 167 | } 168 | 169 | // Search for a record set with the given attr=val. 170 | // Returns no records (nil) if not found. 171 | func (n *Ndb) Search(attr, val string) RecordSet { 172 | var results RecordSet 173 | 174 | // check each db file 175 | for db := n; db != nil; db = db.next { 176 | 177 | // and check each record 178 | for _, record := range db.records { 179 | 180 | // each each tuple! 181 | for _, tuple := range record { 182 | // if val is "" we don't care what it is 183 | if val == "" && tuple.Attr == attr { 184 | results = append(results, record) 185 | } else if tuple.Attr == attr && tuple.Val == val { 186 | results = append(results, record) 187 | } 188 | } 189 | } 190 | 191 | } 192 | 193 | return results 194 | } 195 | 196 | // Parse whole ndb records from the ndb 197 | func parserec(n *Ndb) (RecordSet, error) { 198 | var err error 199 | 200 | records := make(RecordSet, 1) 201 | 202 | n.data.Seek(0, 0) 203 | 204 | scanl := bufio.NewScanner(n.data) 205 | 206 | var rec Record 207 | 208 | for scanl.Scan() { 209 | line := scanl.Text() 210 | 211 | // skip empty lines 212 | if line == "" { 213 | continue 214 | } 215 | 216 | first, _ := utf8.DecodeRuneInString(line) 217 | 218 | // comment, skip 219 | if first == '#' { 220 | continue 221 | } 222 | 223 | // not whitespace, begin a record 224 | if !unicode.IsSpace(first) { 225 | records = append(records, rec) 226 | rec = Record{} 227 | } 228 | 229 | if tuples, terr := parsetuples(line); err != nil { 230 | err = terr 231 | break 232 | } else { 233 | rec = append(rec, tuples...) 234 | } 235 | 236 | } 237 | 238 | if err := scanl.Err(); err != nil { 239 | return nil, err 240 | } 241 | 242 | // make sure to get the last record. 243 | records = append(records, rec) 244 | 245 | return records, err 246 | } 247 | 248 | // bufio.Scanner function to split data by words and quoted strings 249 | func scanStrings(data []byte, atEOF bool) (advance int, token []byte, err error) { 250 | // Skip leading spaces. 251 | start := 0 252 | for width := 0; start < len(data); start += width { 253 | var r rune 254 | r, width = utf8.DecodeRune(data[start:]) 255 | if !unicode.IsSpace(r) { 256 | break 257 | } 258 | } 259 | 260 | if atEOF && len(data) == 0 { 261 | return 0, nil, nil 262 | } 263 | 264 | // Scan until space, marking end of word. 265 | inquote := false 266 | for width, i := 0, start; i < len(data); i += width { 267 | var r rune 268 | r, width = utf8.DecodeRune(data[i:]) 269 | if r == '"' { 270 | inquote = !inquote 271 | continue 272 | } 273 | if unicode.IsSpace(r) && !inquote { 274 | return i + width, data[start:i], nil 275 | } 276 | } 277 | // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. 278 | if atEOF && len(data) > start { 279 | return len(data), data[start:], nil 280 | } 281 | // Request more data. 282 | return 0, nil, nil 283 | } 284 | 285 | // split up a string into ndb tuples. 286 | // parse "quoted strings" correctly, and 287 | // ignore comments at end of line 288 | func parsetuples(line string) ([]Tuple, error) { 289 | tuples := make([]Tuple, 0) 290 | 291 | // only chop comment if it is at the beginning of a line 292 | // TODO: make comments work anywhere not in quotes 293 | if line[0] == '#' { 294 | return tuples, nil 295 | } 296 | 297 | scanw := bufio.NewScanner(strings.NewReader(line)) 298 | scanw.Split(scanStrings) 299 | 300 | for scanw.Scan() { 301 | tpstr := scanw.Text() 302 | //fmt.Printf("tuple %q\n", tpstr) 303 | spl := strings.SplitN(tpstr, "=", 2) 304 | 305 | if len(spl) != 2 { 306 | return nil, fmt.Errorf("invalid tuple %q", tpstr) 307 | } 308 | 309 | spl[1] = strings.TrimLeft(spl[1], `"`) 310 | spl[1] = strings.TrimRight(spl[1], `"`) 311 | 312 | tuples = append(tuples, Tuple{spl[0], spl[1]}) 313 | } 314 | 315 | return tuples, nil 316 | } 317 | -------------------------------------------------------------------------------- /ndb_test.go: -------------------------------------------------------------------------------- 1 | package ndb 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | testndb = "testndb/local" 11 | ) 12 | 13 | type NdbParseTest struct { 14 | line string 15 | 16 | ntup int 17 | tuples []Tuple 18 | } 19 | 20 | var ( 21 | parsetests = []NdbParseTest{ 22 | NdbParseTest{ 23 | line: `irc= host=irc.foo.org port=6697 ssl=true 24 | nick=gordon user=gordon real=gordon 25 | channels="#foo #bar" 26 | 27 | wtmp= file=/var/log/wtmp 28 | mailwatch= dir=/home/mischief/Maildir 29 | markov= order=2 nword=30 corpus=data/corpus`, 30 | ntup: 16, 31 | tuples: []Tuple{Tuple{"irc", ""}, Tuple{"host", "irc.foo.org"}}, 32 | }, 33 | NdbParseTest{ 34 | line: `one=one`, 35 | ntup: 1, 36 | tuples: []Tuple{Tuple{"one", "one"}}, 37 | }, 38 | } 39 | ) 40 | 41 | func TestParseTuples(t *testing.T) { 42 | 43 | for tno, test := range parsetests { 44 | tup, err := parsetuples(test.line) 45 | 46 | t.Logf("%q -> %+v", test.line, tup) 47 | 48 | if err != nil { 49 | t.Error(err) 50 | continue 51 | } 52 | 53 | if len(tup) != test.ntup { 54 | t.Errorf("test %d: expected %d records got %d", tno, test.ntup, len(tup)) 55 | continue 56 | } 57 | 58 | for n, tuple := range test.tuples { 59 | if tup[n].Attr != tuple.Attr { 60 | t.Errorf("test %d: tuple %d: expected attr %q got %q", tno, n, tuple.Attr, tup[n].Attr) 61 | } 62 | 63 | if tup[n].Val != tuple.Val { 64 | t.Errorf("test %d: tuple %d: expected val %q got %q", tno, n, tuple.Val, tup[n].Val) 65 | } 66 | } 67 | } 68 | 69 | //t.Logf("%+v", tup) 70 | } 71 | 72 | func TestParseRecord(t *testing.T) { 73 | data, err := ioutil.ReadFile(testndb) 74 | 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | ndb := &Ndb{data: bytes.NewReader(data)} 80 | rec, err := parserec(ndb) 81 | 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | for _, record := range rec { 87 | 88 | for n, tuple := range record { 89 | if n == 0 { 90 | t.Logf("%+v", tuple) 91 | } else { 92 | t.Logf(" %+v", tuple) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func TestNdbOpen(t *testing.T) { 99 | ndb, err := Open(testndb) 100 | 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | 105 | dbfs := []string{"testndb/local", "testndb/common"} 106 | 107 | for _, dbf := range dbfs { 108 | if ndb.filename != dbf { 109 | t.Fatalf("wrong db file: expected %q got %q", dbf, ndb.filename) 110 | } 111 | ndb = ndb.next 112 | } 113 | 114 | } 115 | 116 | func TestNdbSearch(t *testing.T) { 117 | ndb, err := Open(testndb) 118 | 119 | if err != nil { 120 | t.Fatal(err) 121 | } 122 | 123 | attr := "tcp" 124 | 125 | recs := ndb.Search(attr, "") 126 | 127 | if recs == nil || len(recs) == 0 { 128 | t.Fatalf("search for %q failed", attr) 129 | } 130 | 131 | for _, rec := range recs { 132 | t.Logf("record %+v:", rec[0]) 133 | for _, tuple := range rec[1:] { 134 | t.Logf(" %+v", tuple) 135 | } 136 | } 137 | 138 | attr = "udp" 139 | recs = ndb.Search(attr, "syslog") 140 | 141 | if syslog := recs.Search("port"); syslog != "514" { 142 | t.Fatalf("expected 514, got %q", syslog) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ndb: Network Database 2 | === 3 | 4 | this is an implementation of ndb(6) from plan 9. 5 | 6 | see [ndb](http://godoc.org/github.com/mischief/ndb) for docs. 7 | 8 | see [ndbquery.go](cmd/ndbquery/ndbquery.go) for an example program. 9 | 10 | see [ndb(6)](http://plan9.bell-labs.com/magic/man2html/6/ndb) for more information. 11 | 12 | -------------------------------------------------------------------------------- /testndb/common: -------------------------------------------------------------------------------- 1 | # 2 | # reduced /lib/ndb/common for testing 3 | # 4 | 5 | # 6 | # real dns root server ips 7 | # 8 | dom=A.ROOT-SERVERS.NET ip=198.41.0.4 9 | dom=B.ROOT-SERVERS.NET ip=192.228.79.201 # jan 2004 10 | dom=C.ROOT-SERVERS.NET ip=192.33.4.12 11 | dom=D.ROOT-SERVERS.NET ip=128.8.10.90 12 | dom=E.ROOT-SERVERS.NET ip=192.203.230.10 13 | dom=F.ROOT-SERVERS.NET ip=192.5.5.241 14 | dom=G.ROOT-SERVERS.NET ip=192.112.36.4 15 | dom=H.ROOT-SERVERS.NET ip=128.63.2.53 16 | dom=I.ROOT-SERVERS.NET ip=192.36.148.17 17 | dom=J.ROOT-SERVERS.NET ip=192.58.128.30 # nov 2002 18 | dom=K.ROOT-SERVERS.NET ip=193.0.14.129 19 | dom=L.ROOT-SERVERS.NET ip=199.7.83.42 # nov 2007 20 | dom=M.ROOT-SERVERS.NET ip=202.12.27.33 21 | 22 | dom=a.gtld-servers.net ip=192.5.6.30 23 | dom=b.gtld-servers.net ip=192.33.14.30 24 | dom=c.gtld-servers.net ip=192.26.92.30 25 | dom=d.gtld-servers.net ip=192.31.80.30 26 | dom=e.gtld-servers.net ip=192.12.94.30 27 | dom=f.gtld-servers.net ip=192.35.51.30 28 | dom=g.gtld-servers.net ip=192.42.93.30 29 | dom=h.gtld-servers.net ip=192.54.112.30 30 | dom=i.gtld-servers.net ip=192.43.172.30 31 | dom=j.gtld-servers.net ip=192.48.79.30 32 | dom=k.gtld-servers.net ip=192.52.178.30 33 | dom=l.gtld-servers.net ip=192.41.162.30 34 | dom=m.gtld-servers.net ip=192.55.83.30 35 | 36 | # 37 | # spam defense. unfortunately, arin doesn't give negative 38 | # rcodes for these non-routable addresses. we'll do it for them. 39 | # 40 | dom=10.in-addr.arpa soa= # rfc1918 zones 41 | dom=16.172.in-addr.arpa soa= 42 | dom=17.172.in-addr.arpa soa= 43 | dom=18.172.in-addr.arpa soa= 44 | dom=19.172.in-addr.arpa soa= 45 | dom=20.172.in-addr.arpa soa= 46 | dom=21.172.in-addr.arpa soa= 47 | dom=22.172.in-addr.arpa soa= 48 | dom=23.172.in-addr.arpa soa= 49 | dom=24.172.in-addr.arpa soa= 50 | dom=25.172.in-addr.arpa soa= 51 | dom=26.172.in-addr.arpa soa= 52 | dom=27.172.in-addr.arpa soa= 53 | dom=28.172.in-addr.arpa soa= 54 | dom=29.172.in-addr.arpa soa= 55 | dom=30.172.in-addr.arpa soa= 56 | dom=31.172.in-addr.arpa soa= 57 | dom=168.192.in-addr.arpa soa= 58 | dom=0.in-addr.arpa soa= # rfc3330 zones 59 | dom=127.in-addr.arpa soa= 60 | dom=254.169.in-addr.arpa soa= 61 | dom=2.0.192.in-addr.arpa soa= 62 | dom=255.255.255.255.in-addr.arpa soa= 63 | dom=d.f.ip6.arpa soa= # rfc4193 recommendation 64 | dom=0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa soa= # rfc4291 zones 65 | dom=1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa soa= 66 | dom=8.e.f.ip6.arpa soa= 67 | dom=9.e.f.ip6.arpa soa= 68 | dom=a.e.f.ip6.arpa soa= 69 | dom=b.e.f.ip6.arpa soa= 70 | refresh=3600 ttl=3600 71 | ns=ns1.cs.bell-labs.com 72 | ns=ns2.cs.bell-labs.com 73 | ipv4proto=4 74 | 75 | # 76 | # services 77 | # 78 | tcp=echo port=7 79 | tcp=http port=80 80 | tcp=https port=443 81 | tcp=invalid 82 | 83 | udp=echo port=7 84 | udp=domain port=53 85 | udp=dns port=53 86 | udp=syslog port=514 87 | 88 | -------------------------------------------------------------------------------- /testndb/local: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # files comprising the database, use as many as you like, see ndb(6) 4 | # 5 | database= 6 | file=testndb/local 7 | file=testndb/common 8 | 9 | auth=sources.cs.bell-labs.com authdom=outside.plan9.bell-labs.com 10 | 11 | # 12 | # because the public demands the name localsource 13 | # 14 | ip=127.0.0.1 sys=localhost dom=localhost 15 | --------------------------------------------------------------------------------