// SPDX-License-Identifier: GPL-2.0-only // Copyright (C) 2020 Michael Crute . All rights reserved. // // Use of this source code is governed by a license that can be found in the // LICENSE file. package six import ( "encoding/csv" "fmt" "io" "net/http" "os" ) const sixFeedUrl = "https://www.seattleix.net/autogen/participants.csv" // Error indicating that a line in the SIX CSV feed could not be parsed. // Contains the actual error as well as the line number on which the error // occurred. type SIXParticipantParseError struct { Line int Err error } // Return the error string for the error func (e *SIXParticipantParseError) Error() string { return fmt.Sprintf("record on line %d: %s", e.Line, e.Err) } // Returns the wrapped error func (e *SIXParticipantParseError) Unwrap() error { return e.Err } // Parser for the SIX participant CSV feed. type SIXParser struct { Records []*SIXParticipant Errors []error asnIndex map[int][]*SIXParticipant headers []string } // Builds a new SIXParser func NewSIXParser() *SIXParser { return &SIXParser{ Records: []*SIXParticipant{}, Errors: []error{}, asnIndex: map[int][]*SIXParticipant{}, headers: nil, } } // After parsing the feed will return a list of participant structs for the // passed ASN as well as a boolean indicating if the participant exists. // Participants may have several connections to the exchange and each // connection is a different participant. func (p *SIXParser) GetParticipantByASN(asn int) ([]*SIXParticipant, bool) { r, ok := p.asnIndex[asn] return r, ok } // Adds a participant to the SIXParticipant struct func (p *SIXParser) AddParticipant(sp *SIXParticipant) { p.Records = append(p.Records, sp) if _, ok := p.asnIndex[sp.ASN]; !ok { p.asnIndex[sp.ASN] = []*SIXParticipant{sp} } else { p.asnIndex[sp.ASN] = append(p.asnIndex[sp.ASN], sp) } } func (p *SIXParser) sliceToHeaderMap(s []string) map[string]string { r := map[string]string{} for i, k := range p.headers { r[k] = s[i] } return r } func (p *SIXParser) addRow(rn int, r []string) { defer func() { if e := recover(); e != nil { p.Errors = append(p.Errors, &SIXParticipantParseError{ Line: rn, Err: e.(error), }) } }() p.AddParticipant(NewSIXParticipantFromData(p.sliceToHeaderMap(r))) } // ASSUMPTION: The correct row size will not change // // Participants that aren't using the route servers tend to have inconsistent // row data. The head and tail of the row are fine and contain the organization // and connection information but the middle area that contains route server // stats generally has 7-9 too few columns. This function will graft the head // and tail onto an appropriately sized row. func (p *SIXParser) fixupRow(r []string) []string { k := make([]string, 49) copy(k[:18], r[:18]) copy(k[42:], r[len(r)-7:]) return k } // Parse an io.Reader containing SIX participant data in CSV format and collect // the results into the parser for later querying. func (p *SIXParser) Parse(fr io.Reader) { rn := 0 cr := csv.NewReader(fr) for { rn++ row, err := cr.Read() if err == io.EOF { return } if err != nil { row = p.fixupRow(row) p.Errors = append(p.Errors, err) } if p.headers == nil { p.headers = row continue } p.addRow(rn, row) } } // Create a new SIXParser and parse the CSV file pointed to by the filename. func ParseSIXCSV(filename string) (*SIXParser, error) { fp, err := os.Open(filename) if err != nil { return nil, err } defer fp.Close() sp := NewSIXParser() sp.Parse(fp) return sp, nil } // Create a new SIXParser and parse the contents of the SIX participant file // locate on the SIX http server. func FetchParseSIXCSV() (*SIXParser, error) { res, err := http.Get(sixFeedUrl) if err != nil { return nil, err } defer res.Body.Close() sp := NewSIXParser() sp.Parse(res.Body) return sp, nil }