routing.go (5027B)
1 // Simple router abstraction. 2 // Implements API routing logic with support for path parameters and query parameters. 3 // Path parameters syntax: '/path-comp1/:param/path-comp3 4 // Use the newOp function to create operations. 5 6 package main 7 8 import ( 9 "fmt" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "net/url" 14 "os" 15 "reflect" 16 "runtime" 17 "strings" 18 ) 19 20 // API (/api) 21 22 type operation struct { 23 name string 24 method string 25 pathComps []string 26 action func(requestParams map[string]string) ([]byte, *apierror) 27 shouldLog bool 28 } 29 30 func newOp(method, path string, action func(requestParams map[string]string) ([]byte, *apierror), shouldLog bool) operation { 31 name := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".")[1] 32 return operation{name, method, splitPath(path), action, shouldLog} 33 } 34 35 func splitPath(path string) []string { 36 urlString := strings.TrimPrefix(path, "/") 37 urlString = strings.TrimSuffix(urlString, "/") 38 components := strings.Split(urlString, "/") 39 var trimmed []string 40 for _, c := range components { 41 if c == "" { 42 continue 43 } 44 trimmed = append(trimmed, c) 45 } 46 return trimmed 47 } 48 49 func matchURL(apiPath string, rURL *url.URL, operationComps []string) (bool, map[string]string) { 50 urlComps := splitPath(strings.TrimPrefix(rURL.Path, apiPath)) 51 if len(urlComps) != len(operationComps) { 52 return false, nil 53 } 54 // Read query params before path params, otherwise it would be possible to overwrite path params 55 params := make(map[string]string) 56 for k, v := range rURL.Query() { 57 if len(v) == 1 { 58 params[k] = v[0] 59 } else { 60 params[k] = strings.Join(v, ",") 61 } 62 } 63 for i := range urlComps { 64 // Operation components starting with ':' denote path params 65 if operationComps[i][0] == ':' { 66 paramName := operationComps[i][1:] 67 params[paramName] = urlComps[i] 68 continue 69 } 70 // Non-param path components must match exactly 71 if operationComps[i] != urlComps[i] { 72 return false, nil 73 } 74 } 75 return true, params 76 } 77 78 func getAPIHandler(path string, apiOps []operation) (string, func(w http.ResponseWriter, r *http.Request)) { 79 // Guarantee trailing slash, otherwise the handler may be registered incorrectly 80 if !strings.HasSuffix(path, "/") { 81 path += "/" 82 } 83 return path, func(w http.ResponseWriter, r *http.Request) { 84 var ( 85 match bool 86 matchingOp *operation 87 params map[string]string 88 ) 89 for _, op := range apiOps { 90 if op.method != r.Method { 91 continue 92 } 93 match, params = matchURL(path, r.URL, op.pathComps) 94 if match { 95 matchingOp = &op 96 break 97 } 98 } 99 if matchingOp == nil { 100 log.Println("404:", r.URL.Path) 101 w.WriteHeader(http.StatusNotFound) 102 return 103 } 104 // Add body as a parameter 105 body, err := ioutil.ReadAll(r.Body) 106 if err != nil { 107 log.Println("error when reading request body") 108 } else if string(body) != "" { 109 params["body"] = string(body) 110 defer r.Body.Close() 111 } 112 body, apierr := matchingOp.action(params) 113 114 if apierr != nil { 115 log.Printf("%v: %v: %v\n", matchingOp.name, params, apierr) 116 // TODO: Header should depend on error type 117 w.WriteHeader(apierr.status) 118 w.Write([]byte(apierr.message)) 119 return 120 } 121 if matchingOp.shouldLog { 122 log.Printf("%v: %v\n", matchingOp.name, params) 123 } 124 if body != nil { 125 w.Write(body) 126 } 127 } 128 } 129 130 type apierror struct { 131 status int 132 message string 133 err error 134 } 135 136 func (e *apierror) Error() string { 137 switch e.status { 138 case http.StatusInternalServerError: 139 return fmt.Sprintf("apierror: internal server error: %v", e.err) 140 default: 141 return fmt.Sprintf("apierror: code=%v, message=%v", e.status, e.message) 142 } 143 } 144 145 func errOperationForbidden(clientName string, passphraseProtected bool) *apierror { 146 message := fmt.Sprintf("'%v' not allowed to perform operation", clientName) 147 if passphraseProtected { 148 message += " without the correct passphrase" 149 } 150 return &apierror{ 151 status: http.StatusForbidden, 152 message: message, 153 } 154 } 155 156 func errRequest(message string) *apierror { 157 return &apierror{ 158 status: http.StatusBadRequest, 159 message: message, 160 } 161 } 162 163 func errInternal(message string, err error) *apierror { 164 return &apierror{ 165 status: http.StatusInternalServerError, 166 message: message, 167 err: err, 168 } 169 } 170 171 // Static assets (Everything other than /api) 172 173 var fileTypeMap = map[string]string{ 174 "css": "text/css", 175 "html": "text/html", 176 "png": "image/png", 177 "jpg": "image/jpg", 178 "js": "application/javascript", 179 "json": "application/json", 180 } 181 182 func handleSite(w http.ResponseWriter, r *http.Request) { 183 fmt.Println(r.URL.Path) 184 path := strings.TrimPrefix(r.URL.Path, "/") 185 if path == "" { 186 path = "index.html" 187 } 188 contents, err := readStaticFile(path) 189 if err != nil { 190 if os.IsNotExist(err) { 191 w.WriteHeader(http.StatusNotFound) 192 w.Write([]byte("404: Not found")) 193 } else { 194 w.WriteHeader(http.StatusInternalServerError) 195 w.Write([]byte("404: Not found")) 196 } 197 return 198 } 199 c := strings.Split(path, ".") 200 contentType, ok := fileTypeMap[c[len(c)-1]] 201 if ok { 202 w.Header().Add("Content-Type", contentType) 203 } 204 w.Write(contents) 205 }