|  | // Copyright (C) 2015 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | // http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | package main | 
|  |  | 
|  | import ( | 
|  | "bufio" | 
|  | "compress/gzip" | 
|  | "encoding/json" | 
|  | "errors" | 
|  | "flag" | 
|  | "io" | 
|  | "io/ioutil" | 
|  | "log" | 
|  | "net" | 
|  | "net/http" | 
|  | "net/url" | 
|  | "regexp" | 
|  | "strings" | 
|  | ) | 
|  |  | 
|  | var ( | 
|  | restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to") | 
|  | port     = flag.String("port", ":8081", "Port to serve HTTP requests on") | 
|  | prod     = flag.Bool("prod", false, "Serve production assets") | 
|  | scheme   = flag.String("scheme", "https", "URL scheme") | 
|  | plugins  = flag.String("plugins", "", "Path to local plugins folder") | 
|  | ) | 
|  |  | 
|  | func main() { | 
|  | flag.Parse() | 
|  |  | 
|  | if *prod { | 
|  | http.Handle("/", http.FileServer(http.Dir("dist"))) | 
|  | } else { | 
|  | http.Handle("/", http.FileServer(http.Dir("app"))) | 
|  | } | 
|  |  | 
|  | http.HandleFunc("/changes/", handleRESTProxy) | 
|  | http.HandleFunc("/accounts/", handleRESTProxy) | 
|  | http.HandleFunc("/config/", handleRESTProxy) | 
|  | http.HandleFunc("/projects/", handleRESTProxy) | 
|  | http.HandleFunc("/accounts/self/detail", handleAccountDetail) | 
|  | if len(*plugins) > 0 { | 
|  | http.Handle("/plugins/", http.StripPrefix("/plugins/", | 
|  | http.FileServer(http.Dir(*plugins)))) | 
|  | log.Println("Local plugins from", *plugins) | 
|  | } else { | 
|  | http.HandleFunc("/plugins/", handleRESTProxy) | 
|  | } | 
|  | log.Println("Serving on port", *port) | 
|  | log.Fatal(http.ListenAndServe(*port, &server{})) | 
|  | } | 
|  |  | 
|  | func handleRESTProxy(w http.ResponseWriter, r *http.Request) { | 
|  | if strings.HasSuffix(r.URL.Path, ".html") { | 
|  | w.Header().Set("Content-Type", "text/html") | 
|  | } else if strings.HasSuffix(r.URL.Path, ".css") { | 
|  | w.Header().Set("Content-Type", "text/css") | 
|  | } else { | 
|  | w.Header().Set("Content-Type", "application/json") | 
|  | } | 
|  | req := &http.Request{ | 
|  | Method: "GET", | 
|  | URL: &url.URL{ | 
|  | Scheme:   *scheme, | 
|  | Host:     *restHost, | 
|  | Opaque:   r.URL.EscapedPath(), | 
|  | RawQuery: r.URL.RawQuery, | 
|  | }, | 
|  | } | 
|  | res, err := http.DefaultClient.Do(req) | 
|  | if err != nil { | 
|  | http.Error(w, err.Error(), http.StatusInternalServerError) | 
|  | return | 
|  | } | 
|  | defer res.Body.Close() | 
|  | w.WriteHeader(res.StatusCode) | 
|  | if _, err := io.Copy(w, patchResponse(r, res)); err != nil { | 
|  | log.Println("Error copying response to ResponseWriter:", err) | 
|  | return | 
|  | } | 
|  | } | 
|  |  | 
|  | func getJsonPropByPath(json map[string]interface{}, path []string) interface{} { | 
|  | prop, path := path[0], path[1:] | 
|  | if json[prop] == nil { | 
|  | return nil | 
|  | } | 
|  | switch json[prop].(type) { | 
|  | case map[string]interface{}: // map | 
|  | return getJsonPropByPath(json[prop].(map[string]interface{}), path) | 
|  | case []interface{}: // array | 
|  | return json[prop].([]interface{}) | 
|  | default: | 
|  | return json[prop].(interface{}) | 
|  | } | 
|  | } | 
|  |  | 
|  | func setJsonPropByPath(json map[string]interface{}, path []string, value interface{}) { | 
|  | prop, path := path[0], path[1:] | 
|  | if json[prop] == nil { | 
|  | return // path not found | 
|  | } | 
|  | if len(path) > 0 { | 
|  | setJsonPropByPath(json[prop].(map[string]interface{}), path, value) | 
|  | } else { | 
|  | json[prop] = value | 
|  | } | 
|  | } | 
|  |  | 
|  | func patchResponse(r *http.Request, res *http.Response) io.Reader { | 
|  | switch r.URL.EscapedPath() { | 
|  | case "/config/server/info": | 
|  | return injectLocalPlugins(res.Body) | 
|  | default: | 
|  | return res.Body | 
|  | } | 
|  | } | 
|  |  | 
|  | func injectLocalPlugins(r io.Reader) io.Reader { | 
|  | if len(*plugins) == 0 { | 
|  | return r | 
|  | } | 
|  | // Skip escape prefix | 
|  | io.CopyN(ioutil.Discard, r, 5) | 
|  | dec := json.NewDecoder(r) | 
|  |  | 
|  | var response map[string]interface{} | 
|  | err := dec.Decode(&response) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  |  | 
|  | // Configuration path in the JSON server response | 
|  | pluginsPath := []string{"plugin", "html_resource_paths"} | 
|  |  | 
|  | htmlResources := getJsonPropByPath(response, pluginsPath).([]interface{}) | 
|  | files, err := ioutil.ReadDir(*plugins) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  | for _, f := range files { | 
|  | if strings.HasSuffix(f.Name(), ".html") { | 
|  | htmlResources = append(htmlResources, "plugins/"+f.Name()) | 
|  | } | 
|  | } | 
|  | setJsonPropByPath(response, pluginsPath, htmlResources) | 
|  |  | 
|  | reader, writer := io.Pipe() | 
|  | go func() { | 
|  | defer writer.Close() | 
|  | io.WriteString(writer, ")]}'") // Write escape prefix | 
|  | err := json.NewEncoder(writer).Encode(&response) | 
|  | if err != nil { | 
|  | log.Fatal(err) | 
|  | } | 
|  | }() | 
|  | return reader | 
|  | } | 
|  |  | 
|  | func handleAccountDetail(w http.ResponseWriter, r *http.Request) { | 
|  | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) | 
|  | } | 
|  |  | 
|  | type gzipResponseWriter struct { | 
|  | io.WriteCloser | 
|  | http.ResponseWriter | 
|  | } | 
|  |  | 
|  | func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter { | 
|  | gz := gzip.NewWriter(w) | 
|  | return &gzipResponseWriter{WriteCloser: gz, ResponseWriter: w} | 
|  | } | 
|  |  | 
|  | func (w gzipResponseWriter) Write(b []byte) (int, error) { | 
|  | return w.WriteCloser.Write(b) | 
|  | } | 
|  |  | 
|  | func (w gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { | 
|  | h, ok := w.ResponseWriter.(http.Hijacker) | 
|  | if !ok { | 
|  | return nil, nil, errors.New("gzipResponseWriter: ResponseWriter does not satisfy http.Hijacker interface") | 
|  | } | 
|  | return h.Hijack() | 
|  | } | 
|  |  | 
|  | type server struct{} | 
|  |  | 
|  | // Any path prefixes that should resolve to index.html. | 
|  | var ( | 
|  | fePaths    = []string{"/q/", "/c/", "/dashboard/", "/admin/"} | 
|  | issueNumRE = regexp.MustCompile(`^\/\d+\/?$`) | 
|  | ) | 
|  |  | 
|  | func (_ *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { | 
|  | log.Printf("%s %s %s %s\n", r.Proto, r.Method, r.RemoteAddr, r.URL) | 
|  | for _, prefix := range fePaths { | 
|  | if strings.HasPrefix(r.URL.Path, prefix) { | 
|  | r.URL.Path = "/" | 
|  | log.Println("Redirecting to /") | 
|  | break | 
|  | } else if match := issueNumRE.Find([]byte(r.URL.Path)); match != nil { | 
|  | r.URL.Path = "/" | 
|  | log.Println("Redirecting to /") | 
|  | break | 
|  | } | 
|  | } | 
|  | if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { | 
|  | http.DefaultServeMux.ServeHTTP(w, r) | 
|  | return | 
|  | } | 
|  | w.Header().Set("Content-Encoding", "gzip") | 
|  | gzw := newGzipResponseWriter(w) | 
|  | defer gzw.Close() | 
|  | http.DefaultServeMux.ServeHTTP(gzw, r) | 
|  | } |