├── .gitpod.yml ├── Dockerfile ├── .gitpod.Dockerfile ├── .github └── workflows │ └── main.yml ├── README.md ├── LICENSE ├── main.go ├── gophermap.go └── server.go /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: echo "Replace me with a build script for the project." 3 | command: echo "Replace me with something that should run on every start, or just 4 | remove me entirely." 5 | image: 6 | file: .gitpod.Dockerfile 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine 2 | 3 | ENV GOPHER_ADDRESS localhost 4 | 5 | EXPOSE 70 6 | VOLUME /public 7 | 8 | COPY . /go/src/gopher 9 | RUN go install gopher 10 | COPY README.md /public 11 | 12 | CMD /go/bin/gopher -d /public/ -p 70 -a ${GOPHER_ADDRESS} 13 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | USER gitpod 4 | 5 | # Install custom tools, runtime, etc. using apt-get 6 | # For example, the command below would install "bastet" - a command line tetris clone: 7 | # 8 | # RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/* 9 | # 10 | # More information: https://www.gitpod.io/docs/config-docker/ 11 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Docker push on Github registry 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v2 15 | - name: Publish Image 16 | uses: matootie/github-docker@v2.2.2 17 | with: 18 | accessToken: ${{ secrets.ACCESS_TOKEN }} 19 | imageName: gopher 20 | imageTag: 0.0.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gopher 2 | 3 | 4 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/prodhe/gopher) 5 | 6 | A server hosting files, dirs and links according to the gopher protocol. 7 | 8 | Because gopherspace deserves more servers. 9 | 10 | Work in progress as a playground for basic networking in go. And readers, 11 | writers, buffers, []byte and whatnot... 12 | 13 | ## Gopher protocol 14 | 15 | [ietf rfc](https://tools.ietf.org/html/rfc1436) 16 | 17 | ## License 18 | 19 | MIT. See LICENSE. 20 | 21 | 22 | ## Docker hub 23 | 24 | [docker hub](https://hub.docker.com/r/prodhe/gopher/) 25 | 26 | ## Docker compose 27 | 28 | ```yml 29 | version: '3.1' 30 | 31 | services: 32 | gopher: 33 | image: prodhe/gopher:latest 34 | ports: 35 | - 70:70 36 | environment: 37 | - GOPHER_ADDRESS=localhost 38 | volumes: 39 | - /opt/gopher/public:/public 40 | 41 | ``` 42 | 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Petter Rodhelind 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. -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | gopher - Set up a server to communicate over gopher 3 | 4 | https://tools.ietf.org/html/rfc1436 5 | */ 6 | package main 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "net" 12 | "os" 13 | "path/filepath" 14 | "strconv" 15 | ) 16 | 17 | var ( 18 | help bool 19 | host string 20 | port int 21 | root string 22 | ) 23 | 24 | func init() { 25 | flag.BoolVar(&help, "h", false, "Show usage") 26 | flag.StringVar(&host, "a", "localhost", "Public host `address`") 27 | flag.IntVar(&port, "p", 70, "Listening `port`") 28 | flag.StringVar(&root, "d", "/var/gopher/", "Root `directory` to serve") 29 | } 30 | 31 | func main() { 32 | flag.Parse() 33 | 34 | if help || root == "" { 35 | fmt.Println("usage: gopher [options]") 36 | flag.PrintDefaults() 37 | os.Exit(1) 38 | } 39 | 40 | // check and correct root directory 41 | if !filepath.IsAbs(root) { 42 | path, err := filepath.Abs(root) 43 | if err != nil { 44 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 45 | } 46 | root = path 47 | } 48 | if root[len(root)-1:] != "/" { 49 | root = root + "/" 50 | } 51 | 52 | addr := net.JoinHostPort("0.0.0.0", strconv.Itoa(port)) 53 | ListenAndServe(addr) 54 | } 55 | -------------------------------------------------------------------------------- /gophermap.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | filename string = "gophermap" 13 | ) 14 | 15 | // Gophermap parses the given file 'fn' and returns a proper gopher list of items 16 | func Gophermap(fn string) List { 17 | var l List 18 | 19 | f, err := os.Open(fn) 20 | defer f.Close() 21 | if err != nil { 22 | return Error("gophermap error") 23 | } 24 | scanner := bufio.NewScanner(f) 25 | for scanner.Scan() { 26 | row := scanner.Text() 27 | if strings.Contains(row, "\t") { 28 | itemtype, cols := parse(row) 29 | p, _ := strconv.Atoi(cols[3]) // port 30 | switch itemtype { 31 | case G_MENU: 32 | fallthrough 33 | case G_TEXT: 34 | i := Row(itemtype, cols[0], cols[1], cols[2], p) 35 | l = append(l, i) 36 | case '!': 37 | if cols[0] == "!" && cols[1] == "list" { 38 | l = append(l, ListDir(filepath.Dir(fn))...) 39 | } 40 | default: 41 | i := Row(G_ERROR, strings.Replace(row, "\t", "\\t", -1), "", "", 0) 42 | l = append(l, i) 43 | } 44 | } else { 45 | l = append(l, Row(G_INFO, row, "", "", 0)) 46 | } 47 | } 48 | 49 | return l 50 | } 51 | 52 | func parse(s string) (byte, []string) { 53 | cols := strings.Split(s, "\t") 54 | itemtype := []byte(cols[0][:1])[0] 55 | f_name := cols[0][1:len(cols[0])] 56 | f_selector := "" 57 | f_host := "" 58 | f_port := "" 59 | 60 | if len(cols) >= 4 { 61 | f_selector = cols[1] 62 | f_host = cols[2] 63 | f_port = cols[3] 64 | } else if len(cols) >= 3 { 65 | f_selector = cols[1] 66 | f_host = cols[2] 67 | } else if len(cols) >= 2 { 68 | f_selector = cols[1] 69 | } 70 | 71 | fields := []string{f_name, f_selector, f_host, f_port} 72 | return itemtype, fields 73 | } 74 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | "path/filepath" 12 | ) 13 | 14 | const ( 15 | G_ERROR byte = '3' 16 | G_INFO byte = 'i' 17 | G_MENU byte = '1' 18 | G_TEXT byte = '0' 19 | ) 20 | 21 | type Item struct { 22 | Type byte 23 | Name string 24 | Selector string 25 | Host string 26 | Port int 27 | } 28 | type List []Item 29 | 30 | func (i Item) String() string { 31 | switch i.Type { 32 | case 'i': 33 | return fmt.Sprintf("%c%s\t\tinfo.host\t1\r\n", 34 | i.Type, i.Name) 35 | default: 36 | return fmt.Sprintf("%c%s\t%s\t%s\t%d\r\n", 37 | i.Type, i.Name, i.Selector, i.Host, i.Port) 38 | } 39 | } 40 | 41 | func (l List) String() string { 42 | var b bytes.Buffer 43 | for _, i := range l { 44 | fmt.Fprint(&b, i) 45 | } 46 | fmt.Fprint(&b, ".\r\n") 47 | return b.String() 48 | } 49 | 50 | // Row returns a gopher item ready to be served 51 | func Row(t byte, n, s, h string, p int) Item { 52 | switch t { 53 | case G_ERROR: 54 | s = "" 55 | h = "error.host" 56 | p = 1 57 | case G_INFO: 58 | s = "" 59 | h = "info.host" 60 | p = 1 61 | case G_MENU: 62 | if h == "" { 63 | h = host 64 | p = port 65 | } 66 | case G_TEXT: 67 | if h == "" { 68 | h = host 69 | p = port 70 | } 71 | default: 72 | return Row(G_ERROR, "Internal server error", "", "", 0) 73 | } 74 | return Item{t, n, s, h, p} 75 | } 76 | 77 | // Exists returns whether the given file or directory exists or not 78 | func Exists(path string) (bool, error) { 79 | _, err := os.Stat(path) 80 | if err == nil { 81 | return true, nil 82 | } 83 | if os.IsNotExist(err) { 84 | return false, nil 85 | } 86 | return true, err 87 | } 88 | 89 | // ListDir scans the given 'path' and returns a gopher list of entries 90 | func ListDir(path string) List { 91 | var l List 92 | count := 0 93 | filepath.Walk(path, (func(p string, info os.FileInfo, err error) error { 94 | if info.IsDir() { 95 | // due to how Walk works, the first folder here is the given path 96 | // itself, and we need to not SkipDir it. So for the first run of 97 | // this recursive Walk function, we will allow it to nest deeper 98 | // by just returning nil 99 | if count > 0 { 100 | l = append(l, Row(G_MENU, info.Name(), p[len(root)-1:], "", 0)) 101 | count++ 102 | return filepath.SkipDir 103 | } 104 | count++ 105 | return nil 106 | } 107 | if info.Name() != "gophermap" { 108 | l = append(l, Row(G_TEXT, info.Name(), p[len(root)-1:], "", 0)) 109 | } 110 | return nil 111 | })) 112 | return l 113 | } 114 | 115 | // ListenAndServe starts a gopher server at 'addr' 116 | func ListenAndServe(addr string) { 117 | ln, err := net.Listen("tcp", addr) 118 | if err != nil { 119 | log.Fatal(err) 120 | } 121 | log.Printf("Serving %s at %s:%d", root, host, port) 122 | for { 123 | conn, err := ln.Accept() 124 | if err != nil { 125 | log.Println(err) 126 | continue 127 | } 128 | go handleConn(conn) 129 | } 130 | } 131 | 132 | // handleConn manages open and close of network conn 133 | func handleConn(c net.Conn) { 134 | defer c.Close() 135 | 136 | buf := bufio.NewReader(c) 137 | req, _, err := buf.ReadLine() 138 | if err != nil { 139 | fmt.Fprint(c, Error("Invalid request.")) 140 | } 141 | 142 | log.Printf("%v: %s", c.RemoteAddr(), req) 143 | 144 | handleRequest(string(req), c) 145 | } 146 | 147 | // handleRequest parses the request and sends an answer 148 | func handleRequest(req string, c net.Conn) { 149 | safe_req := filepath.Clean("/" + req) 150 | req = root + safe_req 151 | fmt.Println(safe_req) 152 | 153 | f, err := os.Open(req) 154 | defer f.Close() 155 | if err != nil { 156 | fmt.Fprint(c, Error("Resource not found.")) 157 | return 158 | } 159 | 160 | fi, _ := f.Stat() 161 | if fi.IsDir() { 162 | var l List 163 | if ok, err := Exists(req + "/gophermap"); ok == true && err == nil { 164 | l = append(l, Gophermap(req+"/gophermap")...) 165 | } else { 166 | l = append(l, ListDir(req)...) 167 | } 168 | fmt.Fprint(c, l) 169 | return 170 | } 171 | 172 | io.Copy(c, f) 173 | return 174 | } 175 | 176 | // responseError returns a full response with a gopher-formatted error 's' 177 | func Error(s string) List { 178 | return List{Row(G_ERROR, s, "", "", 0)} 179 | } 180 | --------------------------------------------------------------------------------