package libgopher import ( "bufio" "errors" "io" "log" "net" "net/url" "strings" ) // Item Types const ( FILE = ItemType('0') // Item is a file DIRECTORY = ItemType('1') // Item is a directory PHONEBOOK = ItemType('2') // Item is a CSO phone-book server ERROR = ItemType('3') // Error BINHEX = ItemType('4') // Item is a BinHexed Macintosh file. DOSARCHIVE = ItemType('5') // Item is DOS binary archive of some sort. (*) UUENCODED = ItemType('6') // Item is a UNIX uuencoded file. INDEXSEARCH = ItemType('7') // Item is an Index-Search server. TELNET = ItemType('8') // Item points to a text-based telnet session. BINARY = ItemType('9') // Item is a binary file! (*) // (*) Client must read until the TCP connection is closed. REDUNDANT = ItemType('+') // Item is a redundant server TN3270 = ItemType('T') // Item points to a text-based tn3270 session. GIF = ItemType('g') // Item is a GIF format graphics file. IMAGE = ItemType('I') // Item is some kind of image file. // non-standard INFO = ItemType('i') // Item is an informational message HTML = ItemType('h') // Item is a HTML document AUDIO = ItemType('s') // Item is an Audio file PNG = ItemType('p') // Item is a PNG Image DOC = ItemType('d') // Item is a Document ) const ( // END represents the terminator used in directory responses END = byte('.') // TAB is the delimiter used to separate item response parts TAB = byte('\t') // CRLF is the delimiter used per line of response item CRLF = "\r\n" // DEFAULT is the default item type DEFAULT = BINARY ) // ItemType represents the type of an item type ItemType byte // Return a human friendly represation of an ItemType func (it ItemType) String() string { switch it { case FILE: return "TXT" case DIRECTORY: return "DIR" case PHONEBOOK: return "PHO" case ERROR: return "ERR" case BINHEX: return "HEX" case DOSARCHIVE: return "ARC" case UUENCODED: return "UUE" case INDEXSEARCH: return "QRY" case TELNET: return "TEL" case BINARY: return "BIN" case REDUNDANT: return "DUP" case TN3270: return "TN3" case GIF: return "GIF" case IMAGE: return "IMG" case INFO: return "NFO" case HTML: return "HTM" case AUDIO: return "SND" case PNG: return "PNG" case DOC: return "DOC" default: return "???" } } // Item describes an entry in a directory listing. type Item struct { Type ItemType `json:"type"` Description string `json:"description"` Selector string `json:"selector"` Host string `json:"host"` Port string `json:"port"` // non-standard extensions (ignored by standard clients) Extras []string `json:"extras"` } // ParseItem parses a line of text into an item func ParseItem(line string) (item *Item, err error) { parts := strings.Split(strings.Trim(line, "\r\n"), "\t") if len(parts[0]) < 1 { return nil, errors.New("no item type: " + string(line)) } item = &Item{ Type: ItemType(parts[0][0]), Description: string(parts[0][1:]), Extras: make([]string, 0), } // Selector if len(parts) > 1 { item.Selector = string(parts[1]) } else { item.Selector = "" } // Host if len(parts) > 2 { item.Host = string(parts[2]) } else { item.Host = "null.host" } // Port if len(parts) > 3 { item.Port = string(parts[3]) } else { item.Port = "0" } // Extras if len(parts) >= 4 { for _, v := range parts[4:] { item.Extras = append(item.Extras, string(v)) } } return } func (i *Item) isDirectoryLike() bool { switch i.Type { case DIRECTORY: return true case INDEXSEARCH: return true default: return false } } // Directory representes a Gopher Menu of Items type Directory struct { Items []*Item `json:"items"` } // Response represents a Gopher resource that // Items contains a non-empty array of Item(s) // for directory types, otherwise the Body // contains the fetched resource (file, image, etc). type Response struct { Type ItemType Dir Directory Body io.Reader } // Get fetches a Gopher resource by URL func Get(uri string) (*Response, error) { u, err := url.Parse(uri) if err != nil { return nil, err } if u.Scheme != "gopher" { return nil, errors.New("invalid scheme for uri") } host := u.Hostname() port := u.Port() if port == "" { port = "70" } var ( Type ItemType Selector string ) path := strings.TrimPrefix(u.Path, "/") if len(path) > 2 { Type = ItemType(path[0]) Selector = path[1:] if u.RawQuery != "" { Selector += "\t" + u.RawQuery } } else if len(path) == 1 { Type = ItemType(path[0]) Selector = "" } else { Type = ItemType(DIRECTORY) Selector = "" } i := Item{Type: Type, Selector: Selector, Host: host, Port: port} res := Response{Type: i.Type} if i.isDirectoryLike() { d, err := i.FetchDirectory() if err != nil { return nil, err } res.Dir = d } else { reader, err := i.FetchFile() if err != nil { return nil, err } res.Body = reader } return &res, nil } // FetchFile fetches data, not directory information. // Calling this on a DIRECTORY Item type // or unsupported type will return an error. func (i *Item) FetchFile() (io.Reader, error) { if i.Type == DIRECTORY { return nil, errors.New("cannot fetch a directory as a file") } conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) if err != nil { return nil, err } _, err = conn.Write([]byte(i.Selector + CRLF)) if err != nil { conn.Close() return nil, err } return conn, nil } // FetchDirectory fetches directory information, not data. // Calling this on an Item whose type is not DIRECTORY will return an error. func (i *Item) FetchDirectory() (Directory, error) { if !i.isDirectoryLike() { return Directory{}, errors.New("cannot fetch a file as a directory") } conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port)) if err != nil { return Directory{}, err } _, err = conn.Write([]byte(i.Selector + CRLF)) if err != nil { return Directory{}, err } reader := bufio.NewReader(conn) scanner := bufio.NewScanner(reader) scanner.Split(bufio.ScanLines) var items []*Item for scanner.Scan() { line := strings.Trim(scanner.Text(), "\r\n") if len(line) == 0 { continue } if len(line) == 1 && line[0] == END { break } item, err := ParseItem(line) if err != nil { log.Printf("Error parsing %q: %q", line, err) continue } items = append(items, item) } return Directory{items}, nil }