Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 1 | // Copyright (C) 2015 The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package main |
| 16 | |
| 17 | import ( |
| 18 | "bufio" |
| 19 | "compress/gzip" |
Viktar Donich | f50fbbf | 2017-06-23 11:08:40 -0700 | [diff] [blame] | 20 | "encoding/json" |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 21 | "errors" |
| 22 | "flag" |
| 23 | "io" |
Viktar Donich | f50fbbf | 2017-06-23 11:08:40 -0700 | [diff] [blame] | 24 | "io/ioutil" |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 25 | "log" |
| 26 | "net" |
| 27 | "net/http" |
| 28 | "net/url" |
| 29 | "regexp" |
| 30 | "strings" |
| 31 | ) |
| 32 | |
| 33 | var ( |
Andrew Bonventre | c6857c5 | 2016-03-29 17:16:09 -0400 | [diff] [blame] | 34 | restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to") |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 35 | port = flag.String("port", ":8081", "Port to serve HTTP requests on") |
| 36 | prod = flag.Bool("prod", false, "Serve production assets") |
Viktar Donich | 501e013 | 2016-11-28 16:59:05 -0800 | [diff] [blame] | 37 | scheme = flag.String("scheme", "https", "URL scheme") |
Viktar Donich | 70222ad | 2017-02-08 11:31:31 -0800 | [diff] [blame] | 38 | plugins = flag.String("plugins", "", "Path to local plugins folder") |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 39 | ) |
| 40 | |
| 41 | func main() { |
| 42 | flag.Parse() |
| 43 | |
| 44 | if *prod { |
| 45 | http.Handle("/", http.FileServer(http.Dir("dist"))) |
| 46 | } else { |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 47 | http.Handle("/", http.FileServer(http.Dir("app"))) |
| 48 | } |
| 49 | |
| 50 | http.HandleFunc("/changes/", handleRESTProxy) |
| 51 | http.HandleFunc("/accounts/", handleRESTProxy) |
Andrew Bonventre | 8d1cde2 | 2015-11-30 18:44:48 -0500 | [diff] [blame] | 52 | http.HandleFunc("/config/", handleRESTProxy) |
Andrew Bonventre | 5caa69d | 2015-12-28 15:22:50 -0500 | [diff] [blame] | 53 | http.HandleFunc("/projects/", handleRESTProxy) |
Andrew Bonventre | b07c0d2 | 2015-12-01 13:48:12 -0500 | [diff] [blame] | 54 | http.HandleFunc("/accounts/self/detail", handleAccountDetail) |
Viktar Donich | 70222ad | 2017-02-08 11:31:31 -0800 | [diff] [blame] | 55 | if len(*plugins) > 0 { |
| 56 | http.Handle("/plugins/", http.StripPrefix("/plugins/", |
| 57 | http.FileServer(http.Dir(*plugins)))) |
Viktar Donich | f50fbbf | 2017-06-23 11:08:40 -0700 | [diff] [blame] | 58 | log.Println("Local plugins from", *plugins) |
Viktar Donich | 70222ad | 2017-02-08 11:31:31 -0800 | [diff] [blame] | 59 | } |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 60 | log.Println("Serving on port", *port) |
| 61 | log.Fatal(http.ListenAndServe(*port, &server{})) |
| 62 | } |
| 63 | |
| 64 | func handleRESTProxy(w http.ResponseWriter, r *http.Request) { |
| 65 | w.Header().Set("Content-Type", "application/json") |
| 66 | req := &http.Request{ |
| 67 | Method: "GET", |
| 68 | URL: &url.URL{ |
Viktar Donich | 501e013 | 2016-11-28 16:59:05 -0800 | [diff] [blame] | 69 | Scheme: *scheme, |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 70 | Host: *restHost, |
| 71 | Opaque: r.URL.EscapedPath(), |
| 72 | RawQuery: r.URL.RawQuery, |
| 73 | }, |
| 74 | } |
| 75 | res, err := http.DefaultClient.Do(req) |
| 76 | if err != nil { |
| 77 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| 78 | return |
| 79 | } |
| 80 | defer res.Body.Close() |
Andrew Bonventre | 69fe793 | 2015-11-24 17:24:53 -0500 | [diff] [blame] | 81 | w.WriteHeader(res.StatusCode) |
Viktar Donich | f50fbbf | 2017-06-23 11:08:40 -0700 | [diff] [blame] | 82 | if _, err := io.Copy(w, patchResponse(r, res)); err != nil { |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 83 | log.Println("Error copying response to ResponseWriter:", err) |
| 84 | return |
| 85 | } |
| 86 | } |
| 87 | |
Viktar Donich | f50fbbf | 2017-06-23 11:08:40 -0700 | [diff] [blame] | 88 | func getJsonPropByPath(json map[string]interface{}, path []string) interface{} { |
| 89 | prop, path := path[0], path[1:] |
| 90 | if json[prop] == nil { |
| 91 | return nil |
| 92 | } |
| 93 | switch json[prop].(type) { |
| 94 | case map[string]interface{}: // map |
| 95 | return getJsonPropByPath(json[prop].(map[string]interface{}), path) |
| 96 | case []interface{}: // array |
| 97 | return json[prop].([]interface{}) |
| 98 | default: |
| 99 | return json[prop].(interface{}) |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | func setJsonPropByPath(json map[string]interface{}, path []string, value interface{}) { |
| 104 | prop, path := path[0], path[1:] |
| 105 | if json[prop] == nil { |
| 106 | return // path not found |
| 107 | } |
| 108 | if len(path) > 0 { |
| 109 | setJsonPropByPath(json[prop].(map[string]interface{}), path, value) |
| 110 | } else { |
| 111 | json[prop] = value |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | func patchResponse(r *http.Request, res *http.Response) io.Reader { |
| 116 | switch r.URL.EscapedPath() { |
| 117 | case "/config/server/info": |
| 118 | return injectLocalPlugins(res.Body) |
| 119 | default: |
| 120 | return res.Body |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func injectLocalPlugins(r io.Reader) io.Reader { |
Viktar Donich | 653b6bc | 2017-06-27 09:42:22 -0700 | [diff] [blame] | 125 | if len(*plugins) == 0 { |
| 126 | return r |
| 127 | } |
Viktar Donich | f50fbbf | 2017-06-23 11:08:40 -0700 | [diff] [blame] | 128 | // Skip escape prefix |
| 129 | io.CopyN(ioutil.Discard, r, 5) |
| 130 | dec := json.NewDecoder(r) |
| 131 | |
| 132 | var response map[string]interface{} |
| 133 | err := dec.Decode(&response) |
| 134 | if err != nil { |
| 135 | log.Fatal(err) |
| 136 | } |
| 137 | |
| 138 | // Configuration path in the JSON server response |
| 139 | pluginsPath := []string{"plugin", "html_resource_paths"} |
| 140 | |
| 141 | htmlResources := getJsonPropByPath(response, pluginsPath).([]interface{}) |
| 142 | files, err := ioutil.ReadDir(*plugins) |
| 143 | if err != nil { |
| 144 | log.Fatal(err) |
| 145 | } |
| 146 | for _, f := range files { |
| 147 | if strings.HasSuffix(f.Name(), ".html") { |
| 148 | htmlResources = append(htmlResources, "plugins/"+f.Name()) |
| 149 | } |
| 150 | } |
| 151 | setJsonPropByPath(response, pluginsPath, htmlResources) |
| 152 | |
| 153 | reader, writer := io.Pipe() |
| 154 | go func() { |
| 155 | defer writer.Close() |
| 156 | io.WriteString(writer, ")]}'") // Write escape prefix |
| 157 | err := json.NewEncoder(writer).Encode(&response) |
| 158 | if err != nil { |
| 159 | log.Fatal(err) |
| 160 | } |
| 161 | }() |
| 162 | return reader |
| 163 | } |
| 164 | |
Andrew Bonventre | b07c0d2 | 2015-12-01 13:48:12 -0500 | [diff] [blame] | 165 | func handleAccountDetail(w http.ResponseWriter, r *http.Request) { |
Andrew Bonventre | 8963eae | 2016-05-06 12:41:33 -0400 | [diff] [blame] | 166 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) |
Andrew Bonventre | b07c0d2 | 2015-12-01 13:48:12 -0500 | [diff] [blame] | 167 | } |
| 168 | |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 169 | type gzipResponseWriter struct { |
| 170 | io.WriteCloser |
| 171 | http.ResponseWriter |
| 172 | } |
| 173 | |
| 174 | func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter { |
| 175 | gz := gzip.NewWriter(w) |
| 176 | return &gzipResponseWriter{WriteCloser: gz, ResponseWriter: w} |
| 177 | } |
| 178 | |
| 179 | func (w gzipResponseWriter) Write(b []byte) (int, error) { |
| 180 | return w.WriteCloser.Write(b) |
| 181 | } |
| 182 | |
| 183 | func (w gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { |
| 184 | h, ok := w.ResponseWriter.(http.Hijacker) |
| 185 | if !ok { |
| 186 | return nil, nil, errors.New("gzipResponseWriter: ResponseWriter does not satisfy http.Hijacker interface") |
| 187 | } |
| 188 | return h.Hijack() |
| 189 | } |
| 190 | |
| 191 | type server struct{} |
| 192 | |
| 193 | // Any path prefixes that should resolve to index.html. |
| 194 | var ( |
Andrew Bonventre | a5fff8e | 2015-11-25 17:04:33 -0500 | [diff] [blame] | 195 | fePaths = []string{"/q/", "/c/", "/dashboard/"} |
Andrew Bonventre | ba69835 | 2015-11-04 11:14:54 -0500 | [diff] [blame] | 196 | issueNumRE = regexp.MustCompile(`^\/\d+\/?$`) |
| 197 | ) |
| 198 | |
| 199 | func (_ *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
| 200 | log.Printf("%s %s %s %s\n", r.Proto, r.Method, r.RemoteAddr, r.URL) |
| 201 | for _, prefix := range fePaths { |
| 202 | if strings.HasPrefix(r.URL.Path, prefix) { |
| 203 | r.URL.Path = "/" |
| 204 | log.Println("Redirecting to /") |
| 205 | break |
| 206 | } else if match := issueNumRE.Find([]byte(r.URL.Path)); match != nil { |
| 207 | r.URL.Path = "/" |
| 208 | log.Println("Redirecting to /") |
| 209 | break |
| 210 | } |
| 211 | } |
| 212 | if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { |
| 213 | http.DefaultServeMux.ServeHTTP(w, r) |
| 214 | return |
| 215 | } |
| 216 | w.Header().Set("Content-Encoding", "gzip") |
| 217 | gzw := newGzipResponseWriter(w) |
| 218 | defer gzw.Close() |
| 219 | http.DefaultServeMux.ServeHTTP(gzw, r) |
| 220 | } |