commit 0398caf9e3322a42e90e07b28b698ffe4dcacd60 Author: sloumdrone Date: Sun Feb 24 17:43:10 2019 -0800 Initial commit of gopher library and surrounding structure diff --git a/gclient.go b/gclient.go new file mode 100644 index 0000000..4e9f9b8 --- /dev/null +++ b/gclient.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "gsock/gopher" + "os" + "bufio" + "regexp" + "strings" + "strconv" +) + +var history gopher.History = gopher.MakeHistory() + +func err_exit(err string, code int) { + fmt.Println(err) + os.Exit(code) +} + +func getln() string { + reader := bufio.NewReader(os.Stdin) + fmt.Print(": ") + text, _ := reader.ReadString('\n') + return text[:len(text)-1] +} + + +func route_input(s string) { + sl := strings.ToLower(s) + if sl == "quit" || sl == "exit" || sl == "q" { + err_exit("Quitting...", 0) + } else if num, _ := regexp.MatchString(`^\d+$`, s); num && history.Length > 0 { + linkcount := len(history.Collection[history.Position].Links) + item, _ := strconv.Atoi(s) + if item <= linkcount { + linkurl := history.Collection[history.Position].Links[item - 1] + + v, err := history.Visit(linkurl) + if err != nil { + fmt.Println(err.Error()) + } + if v.Address.IsBinary { + // Query for download here + fmt.Println("Would you like to download this file?") + } else { + history.Add(v) + history.DisplayCurrentView() + } + } else { + fmt.Println("Invalid link id") + } + } else if sl == "back" || sl == "b" { + history.GoBack() + } else if sl == "forward" || sl == "f" { + history.GoForward() + } else { + v, err := history.Visit(s) + if err != nil { + fmt.Println(err.Error()) + } + if v.Address.IsBinary { + // Query for download here + fmt.Println("Would you like to download this file?") + } else { + history.Add(v) + history.DisplayCurrentView() + } + } +} + +func make_request(s string) ([]string, gopher.Url, error) { + u, _ := gopher.MakeUrl(s) + text, err := gopher.Retrieve(u) + if err != nil { + return []string{}, u, err + } + return strings.Split(string(text), "\n"), u, nil +} + +func main() { + history.Position = -1 + var inp string + if len(os.Args) >= 2 { + inp = os.Args[1] + route_input(inp) + } + + for { + inp = getln() + if inp == "" { + continue + } + route_input(inp) + } +} diff --git a/gopher/gopher.go b/gopher/gopher.go new file mode 100644 index 0000000..0573b2b --- /dev/null +++ b/gopher/gopher.go @@ -0,0 +1,343 @@ +// Contains the building blocks of a gopher client: history and view. +// History handles the browsing session and view represents individual +// text based resources. +package gopher + +import ( + "fmt" + "strings" + "errors" + "regexp" + "net" + "io/ioutil" + "time" +) + + +//------------------------------------------------\\ +// + + + T Y P E S + + + \\ +//--------------------------------------------------\\ + +// The history struct represents the history of the browsing +// session. It contains the current history position, the +// length of the active history space (this can be different +// from the available capacity in the Collection), and a +// collection array containing View structs representing +// each page in the current history. In general usage this +// struct should be initialized via the MakeHistory function. +type History struct { + Position int + Length int + Collection [20]View +} + +// The view struct represents a gopher page. It contains +// the page content as a string slice, a list of link URLs +// as string slices, and the Url struct representing the page. +type View struct { + Content []string + Links []string + Address Url +} + +// The url struct represents a URL for the rest of the system. +// It includes component parts as well as a full URL string. +type Url struct { + Scheme string + Host string + Port string + Gophertype string + Resource string + Full string + IsBinary bool +} + + + +//------------------------------------------------\\ +// + + + V A R I A B L E S + + + \\ +//--------------------------------------------------\\ + +// Types is a map of gophertypes to a string representing their +// type, to be used when displaying gophermaps +var Types = map[string]string{ + "0": "TXT", + "1": "MAP", + "h": "HTM", + "3": "ERR", + "4": "BIN", + "5": "DOS", + "s": "SND", + "g": "GIF", + "I": "IMG", + "9": "BIN", + "7": "FTS", + "6": "UUE", + "p": "PNG", +} + + +//------------------------------------------------\\ +// + + + R E C E I V E R S + + + \\ +//--------------------------------------------------\\ + +// The "Add" receiver takes a view and adds it to +// the history struct that called it. "Add" returns +// nothing. "Add" will shift history down if the max +// history length would be exceeded, and will reset +// history length if something is added in the middle. +func (h *History) Add(v View) { + v.ParseMap() + if h.Position == h.Length - 1 && h.Length < len(h.Collection) { + h.Collection[h.Length] = v + h.Length++ + h.Position++ + } else if h.Position == h.Length - 1 && h.Length == 20 { + for x := 1; x < len(h.Collection); x++ { + h.Collection[x-1] = h.Collection[x] + } + h.Collection[len(h.Collection)-1] = v + } else { + h.Position += 1 + h.Length = h.Position + 1 + h.Collection[h.Position] = v + } +} + +// The "Get" receiver is called by a history struct +// and returns a View from the current position, will +// return an error if history is empty and there is +// nothing to get. +func (h History) Get() (*View, error) { + if h.Position < 0 { + return nil, errors.New("History is empty, cannot get item from empty history.") + } + + return &h.Collection[h.Position], nil +} + +// The "GoBack" receiver is called by a history struct. +// When called it decrements the current position and +// displays the content for the View in that position. +// If history is at position 0, no action is taken. +func (h *History) GoBack() { + if h.Position > 0 { + h.Position-- + h.DisplayCurrentView() + } +} + + +// The "GoForward" receiver is called by a history struct. +// When called it increments the current position and +// displays the content for the View in that position. +// If history is at position len - 1, no action is taken. +func (h *History) GoForward() { + if h.Position + 1 < h.Length { + h.Position++ + h.DisplayCurrentView() + } +} + +// The "DisplayCurrentView" receiver is called by a history +// struct. It calls the Display receiver for th view struct +// at the current history position. "DisplayCurrentView" does +// not return anything, and does nothing if position is less +// that 0. +func (h *History) DisplayCurrentView() { + h.Collection[h.Position].Display() +} + +// The "Visit" receiver is a high level combination of a few +// different receivers that makes it easy to create a Url, +// make a request to that Url, and add the response and Url +// to a View. That View then gets added to the History struct +// that the Visit receiver was called on. Returns a boolean +// value indicating whether or not the content is binary or +// textual data. +func (h *History) Visit(addr string) (View, error) { + u, err := MakeUrl(addr) + if err != nil { + return View{}, err + } + + text, err := Retrieve(u) + if err != nil { + return View{}, err + } + + var pageContent []string + if u.IsBinary { + pageContent = []string{string(text)} + } else { + pageContent = strings.Split(string(text), "\n") + } + + return MakeView(u, pageContent), nil +} + +// The "ParseMap" receiver is called by a view struct. It +// checks if the view is for a gophermap. If not,it does +// nothing. If so, it parses the gophermap into comment lines +// and link lines. For link lines it adds a link to the links +// slice and changes the content value to just the printable +// string plus a gophertype indicator and a link number that +// relates to the link position in the links slice. This +// receiver does not return anything. +func (v *View) ParseMap() { + if v.Address.Gophertype == "1" { + for i, e := range v.Content { + e = strings.Trim(e, "\r\n") + line := strings.Split(e,"\t") + if len(line[0]) > 0 && string(line[0][0]) == "i" { + v.Content[i] = " " + string(line[0][1:]) + continue + } else if len(line) >= 4 { + fulllink := fmt.Sprintf("%s:%s/%s%s", line[2], line[3], string(line[0][0]), line[1]) + v.Links = append(v.Links, fulllink) + linktext := fmt.Sprintf("(%s) %2d %s", Types[string(line[0][0])], len(v.Links), string(line[0][1:])) + v.Content[i] = linktext + } + } + } +} + +// The "Display" receiver is called on a view struct. +// It prints the content, line by line, of the View. +// This receiver does not return anything. +func (v View) Display() { + fmt.Println() + for _, el := range v.Content { + if el != "." { + fmt.Println(el) + } + } +} + + +//------------------------------------------------\\ +// + + + F U N C T I O N S + + + \\ +//--------------------------------------------------\\ + +// MakeUrl is a Url constructor that takes in a string +// representation of a url and returns a Url struct and +// an error (or nil). +func MakeUrl(u string) (Url, error) { + var out Url + re := regexp.MustCompile(`^((?Pgopher|http|https|ftp|telnet):\/\/)?(?P[\w\-\.\d]+)(?::(?P\d+)?)?(?:/(?P[01345679gIhisp])?)?(?P(?:\/.*)?)?$`) + match := re.FindStringSubmatch(u) + + if valid := re.MatchString(u); valid != true { + return out, errors.New("Invalid URL or command character") + } + + for i, name := range re.SubexpNames() { + switch name { + case "scheme": + out.Scheme = match[i] + case "host": + out.Host = match[i] + case "port": + out.Port = match[i] + case "type": + out.Gophertype = match[i] + case "resource": + out.Resource = match[i] + } + } + + if out.Scheme == "" { + out.Scheme = "gopher" + } + + if out.Host == "" { + return out, errors.New("No host.") + } + + if out.Scheme == "gopher" && out.Port == "" { + out.Port = "70" + } else if out.Scheme == "http" || out.Scheme == "https" && out.Port == "" { + out.Port = "80" + } + + if out.Gophertype == "" && (out.Resource == "" || out.Resource == "/") { + out.Gophertype = "1" + } + + if out.Gophertype == "1" || out.Gophertype == "0" { + out.IsBinary = false + } else { + out.IsBinary = true + } + + if out.Scheme == "gopher" && out.Gophertype == "" { + out.Gophertype = "0" + } + + out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Gophertype + out.Resource + + return out, nil +} + + +// Constructor function for History struct. +// This is used to initialize history position +// as -1, which is needed. Returns a copy of +// initialized History struct (does NOT return +// a pointer to the struct). +func MakeHistory() History { + return History{-1, 0, [20]View{}} +} + + +// Constructor function for View struct. +// This is used to initialize a View with +// a Url struct, links, and content. It takes +// a Url struct and a content []string and returns +// a View (NOT a pointer to a View). +func MakeView(url Url, content []string) View { + v := View{content, make([]string, 0), url} + v.ParseMap() + return v +} + + +// Retrieve makes a request to a Url and resturns +// the response as []byte/error. This function is +// available to use directly, but in most implementations +// using the "Visit" receiver of the History struct will +// be better. +func Retrieve(u Url) ([]byte, error) { + nullRes := make([]byte, 0) + timeOut := time.Duration(5) * time.Second + + if u.Host == "" || u.Port == "" { + return nullRes, errors.New("Incomplete request url") + } + + addr := u.Host + ":" + u.Port + + conn, err := net.DialTimeout("tcp", addr, timeOut) + if err != nil { + return nullRes, err + } + + send := u.Resource + "\n" + if u.Scheme == "http" || u.Scheme == "https" { + send = u.Gophertype + } + + _, err = conn.Write([]byte(send)) + if err != nil { + return nullRes, err + } + + result, err := ioutil.ReadAll(conn) + if err != nil { + return nullRes, err + } + + return result, err +} + + diff --git a/socket/socket.go b/socket/socket.go new file mode 100644 index 0000000..06eb570 --- /dev/null +++ b/socket/socket.go @@ -0,0 +1,48 @@ +package socket + +import ( + "net" + "io/ioutil" + "gsock/gopher" + "errors" +) + + + +func Retrieve(u gopher.Url) ([]byte, error) { + nullRes := make([]byte, 0) + if u.Host == "" || u.Port == "" { + return nullRes, errors.New("Incomplete request url") + } + + addr := u.Host + ":" + u.Port + tcpAddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + errortext := "Could not find host: " + u.Full + return nullRes, errors.New(errortext) + } + + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + return nullRes, err + } + + send := u.Resource + "\n" + if u.Scheme == "http" || u.Scheme == "https" { + send = u.Gophertype + } + + _, err = conn.Write([]byte(send)) + if err != nil { + return nullRes, err + } + + result, err := ioutil.ReadAll(conn) + if err != nil { + return nullRes, err + } + + return result, err +} + +