aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/libgopher
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/libgopher')
-rw-r--r--pkg/libgopher/libgopher.go312
1 files changed, 312 insertions, 0 deletions
diff --git a/pkg/libgopher/libgopher.go b/pkg/libgopher/libgopher.go
new file mode 100644
index 0000000..86d58ff
--- /dev/null
+++ b/pkg/libgopher/libgopher.go
@@ -0,0 +1,312 @@
1package libgopher
2
3import (
4 "bufio"
5 "errors"
6 "io"
7 "log"
8 "net"
9 "net/url"
10 "strings"
11)
12
13// Item Types
14const (
15 FILE = ItemType('0') // Item is a file
16 DIRECTORY = ItemType('1') // Item is a directory
17 PHONEBOOK = ItemType('2') // Item is a CSO phone-book server
18 ERROR = ItemType('3') // Error
19 BINHEX = ItemType('4') // Item is a BinHexed Macintosh file.
20 DOSARCHIVE = ItemType('5') // Item is DOS binary archive of some sort. (*)
21 UUENCODED = ItemType('6') // Item is a UNIX uuencoded file.
22 INDEXSEARCH = ItemType('7') // Item is an Index-Search server.
23 TELNET = ItemType('8') // Item points to a text-based telnet session.
24 BINARY = ItemType('9') // Item is a binary file! (*)
25
26 // (*) Client must read until the TCP connection is closed.
27
28 REDUNDANT = ItemType('+') // Item is a redundant server
29 TN3270 = ItemType('T') // Item points to a text-based tn3270 session.
30 GIF = ItemType('g') // Item is a GIF format graphics file.
31 IMAGE = ItemType('I') // Item is some kind of image file.
32
33 // non-standard
34 INFO = ItemType('i') // Item is an informational message
35 HTML = ItemType('h') // Item is a HTML document
36 AUDIO = ItemType('s') // Item is an Audio file
37 PNG = ItemType('p') // Item is a PNG Image
38 DOC = ItemType('d') // Item is a Document
39)
40
41const (
42 // END represents the terminator used in directory responses
43 END = byte('.')
44
45 // TAB is the delimiter used to separate item response parts
46 TAB = byte('\t')
47
48 // CRLF is the delimiter used per line of response item
49 CRLF = "\r\n"
50
51 // DEFAULT is the default item type
52 DEFAULT = BINARY
53)
54
55// ItemType represents the type of an item
56type ItemType byte
57
58// Return a human friendly represation of an ItemType
59func (it ItemType) String() string {
60 switch it {
61 case FILE:
62 return "TXT"
63 case DIRECTORY:
64 return "DIR"
65 case PHONEBOOK:
66 return "PHO"
67 case ERROR:
68 return "ERR"
69 case BINHEX:
70 return "HEX"
71 case DOSARCHIVE:
72 return "ARC"
73 case UUENCODED:
74 return "UUE"
75 case INDEXSEARCH:
76 return "QRY"
77 case TELNET:
78 return "TEL"
79 case BINARY:
80 return "BIN"
81 case REDUNDANT:
82 return "DUP"
83 case TN3270:
84 return "TN3"
85 case GIF:
86 return "GIF"
87 case IMAGE:
88 return "IMG"
89 case INFO:
90 return "NFO"
91 case HTML:
92 return "HTM"
93 case AUDIO:
94 return "SND"
95 case PNG:
96 return "PNG"
97 case DOC:
98 return "DOC"
99 default:
100 return "???"
101 }
102}
103
104// Item describes an entry in a directory listing.
105type Item struct {
106 Type ItemType `json:"type"`
107 Description string `json:"description"`
108 Selector string `json:"selector"`
109 Host string `json:"host"`
110 Port string `json:"port"`
111
112 // non-standard extensions (ignored by standard clients)
113 Extras []string `json:"extras"`
114}
115
116// ParseItem parses a line of text into an item
117func ParseItem(line string) (item *Item, err error) {
118 parts := strings.Split(strings.Trim(line, "\r\n"), "\t")
119
120 if len(parts[0]) < 1 {
121 return nil, errors.New("no item type: " + string(line))
122 }
123
124 item = &Item{
125 Type: ItemType(parts[0][0]),
126 Description: string(parts[0][1:]),
127 Extras: make([]string, 0),
128 }
129
130 // Selector
131 if len(parts) > 1 {
132 item.Selector = string(parts[1])
133 } else {
134 item.Selector = ""
135 }
136
137 // Host
138 if len(parts) > 2 {
139 item.Host = string(parts[2])
140 } else {
141 item.Host = "null.host"
142 }
143
144 // Port
145 if len(parts) > 3 {
146 item.Port = string(parts[3])
147 } else {
148 item.Port = "0"
149 }
150
151 // Extras
152 if len(parts) >= 4 {
153 for _, v := range parts[4:] {
154 item.Extras = append(item.Extras, string(v))
155 }
156 }
157
158 return
159}
160
161func (i *Item) isDirectoryLike() bool {
162 switch i.Type {
163 case DIRECTORY:
164 return true
165 case INDEXSEARCH:
166 return true
167 default:
168 return false
169 }
170}
171
172// Directory representes a Gopher Menu of Items
173type Directory struct {
174 Items []*Item `json:"items"`
175}
176
177// Response represents a Gopher resource that
178// Items contains a non-empty array of Item(s)
179// for directory types, otherwise the Body
180// contains the fetched resource (file, image, etc).
181type Response struct {
182 Type ItemType
183 Dir Directory
184 Body io.Reader
185}
186
187// Get fetches a Gopher resource by URI
188func Get(uri string) (*Response, error) {
189 u, err := url.Parse(uri)
190 if err != nil {
191 return nil, err
192 }
193
194 if u.Scheme != "gopher" {
195 return nil, errors.New("invalid scheme for uri")
196 }
197
198 host := u.Hostname()
199 port := u.Port()
200
201 if port == "" {
202 port = "70"
203 }
204
205 var (
206 Type ItemType
207 Selector string
208 )
209
210 path := strings.TrimPrefix(u.Path, "/")
211 if len(path) > 2 {
212 Type = ItemType(path[0])
213 Selector = path[1:]
214 if u.RawQuery != "" {
215 Selector += "\t" + u.RawQuery
216 }
217 } else if len(path) == 1 {
218 Type = ItemType(path[0])
219 Selector = ""
220 } else {
221 Type = ItemType(DIRECTORY)
222 Selector = ""
223 }
224
225 i := Item{Type: Type, Selector: Selector, Host: host, Port: port}
226 res := Response{Type: i.Type}
227
228 if i.isDirectoryLike() {
229 d, err := i.FetchDirectory()
230 if err != nil {
231 return nil, err
232 }
233
234 res.Dir = d
235 } else {
236 reader, err := i.FetchFile()
237 if err != nil {
238 return nil, err
239 }
240
241 res.Body = reader
242 }
243
244 return &res, nil
245}
246
247// FetchFile fetches data, not directory information.
248// Calling this on a DIRECTORY Item type
249// or unsupported type will return an error.
250func (i *Item) FetchFile() (io.Reader, error) {
251 if i.Type == DIRECTORY {
252 return nil, errors.New("cannot fetch a directory as a file")
253 }
254
255 conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port))
256 if err != nil {
257 return nil, err
258 }
259
260 _, err = conn.Write([]byte(i.Selector + CRLF))
261 if err != nil {
262 conn.Close()
263 return nil, err
264 }
265
266 return conn, nil
267}
268
269// FetchDirectory fetches directory information, not data.
270// Calling this on an Item whose type is not DIRECTORY will return an error.
271func (i *Item) FetchDirectory() (Directory, error) {
272 if !i.isDirectoryLike() {
273 return Directory{}, errors.New("cannot fetch a file as a directory")
274 }
275
276 conn, err := net.Dial("tcp", net.JoinHostPort(i.Host, i.Port))
277 if err != nil {
278 return Directory{}, err
279 }
280
281 _, err = conn.Write([]byte(i.Selector + CRLF))
282 if err != nil {
283 return Directory{}, err
284 }
285
286 reader := bufio.NewReader(conn)
287 scanner := bufio.NewScanner(reader)
288 scanner.Split(bufio.ScanLines)
289
290 var items []*Item
291
292 for scanner.Scan() {
293 line := strings.Trim(scanner.Text(), "\r\n")
294
295 if len(line) == 0 {
296 continue
297 }
298
299 if len(line) == 1 && line[0] == END {
300 break
301 }
302
303 item, err := ParseItem(line)
304 if err != nil {
305 log.Printf("Error parsing %q: %q", line, err)
306 continue
307 }
308 items = append(items, item)
309 }
310
311 return Directory{items}, nil
312}