blob: a42acc3d210d67f8ce5e601ec0ad66aa304ef1f2 [file] [log] [blame]
Andrew Bonventreba698352015-11-04 11:14:54 -05001// 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
15package main
16
17import (
18 "bufio"
19 "compress/gzip"
Viktar Donichf50fbbf2017-06-23 11:08:40 -070020 "encoding/json"
Andrew Bonventreba698352015-11-04 11:14:54 -050021 "errors"
22 "flag"
23 "io"
Viktar Donichf50fbbf2017-06-23 11:08:40 -070024 "io/ioutil"
Andrew Bonventreba698352015-11-04 11:14:54 -050025 "log"
26 "net"
27 "net/http"
28 "net/url"
29 "regexp"
30 "strings"
31)
32
33var (
Andrew Bonventrec6857c52016-03-29 17:16:09 -040034 restHost = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
Andrew Bonventreba698352015-11-04 11:14:54 -050035 port = flag.String("port", ":8081", "Port to serve HTTP requests on")
36 prod = flag.Bool("prod", false, "Serve production assets")
Viktar Donich501e0132016-11-28 16:59:05 -080037 scheme = flag.String("scheme", "https", "URL scheme")
Viktar Donich70222ad2017-02-08 11:31:31 -080038 plugins = flag.String("plugins", "", "Path to local plugins folder")
Andrew Bonventreba698352015-11-04 11:14:54 -050039)
40
41func main() {
42 flag.Parse()
43
44 if *prod {
45 http.Handle("/", http.FileServer(http.Dir("dist")))
46 } else {
Andrew Bonventreba698352015-11-04 11:14:54 -050047 http.Handle("/", http.FileServer(http.Dir("app")))
48 }
49
50 http.HandleFunc("/changes/", handleRESTProxy)
51 http.HandleFunc("/accounts/", handleRESTProxy)
Andrew Bonventre8d1cde22015-11-30 18:44:48 -050052 http.HandleFunc("/config/", handleRESTProxy)
Andrew Bonventre5caa69d2015-12-28 15:22:50 -050053 http.HandleFunc("/projects/", handleRESTProxy)
Andrew Bonventreb07c0d22015-12-01 13:48:12 -050054 http.HandleFunc("/accounts/self/detail", handleAccountDetail)
Viktar Donich70222ad2017-02-08 11:31:31 -080055 if len(*plugins) > 0 {
56 http.Handle("/plugins/", http.StripPrefix("/plugins/",
57 http.FileServer(http.Dir(*plugins))))
Viktar Donichf50fbbf2017-06-23 11:08:40 -070058 log.Println("Local plugins from", *plugins)
Viktar Donich70222ad2017-02-08 11:31:31 -080059 }
Andrew Bonventreba698352015-11-04 11:14:54 -050060 log.Println("Serving on port", *port)
61 log.Fatal(http.ListenAndServe(*port, &server{}))
62}
63
64func 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 Donich501e0132016-11-28 16:59:05 -080069 Scheme: *scheme,
Andrew Bonventreba698352015-11-04 11:14:54 -050070 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 Bonventre69fe7932015-11-24 17:24:53 -050081 w.WriteHeader(res.StatusCode)
Viktar Donichf50fbbf2017-06-23 11:08:40 -070082 if _, err := io.Copy(w, patchResponse(r, res)); err != nil {
Andrew Bonventreba698352015-11-04 11:14:54 -050083 log.Println("Error copying response to ResponseWriter:", err)
84 return
85 }
86}
87
Viktar Donichf50fbbf2017-06-23 11:08:40 -070088func 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
103func 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
115func 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
124func injectLocalPlugins(r io.Reader) io.Reader {
Viktar Donich653b6bc2017-06-27 09:42:22 -0700125 if len(*plugins) == 0 {
126 return r
127 }
Viktar Donichf50fbbf2017-06-23 11:08:40 -0700128 // 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 Bonventreb07c0d22015-12-01 13:48:12 -0500165func handleAccountDetail(w http.ResponseWriter, r *http.Request) {
Andrew Bonventre8963eae2016-05-06 12:41:33 -0400166 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
Andrew Bonventreb07c0d22015-12-01 13:48:12 -0500167}
168
Andrew Bonventreba698352015-11-04 11:14:54 -0500169type gzipResponseWriter struct {
170 io.WriteCloser
171 http.ResponseWriter
172}
173
174func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter {
175 gz := gzip.NewWriter(w)
176 return &gzipResponseWriter{WriteCloser: gz, ResponseWriter: w}
177}
178
179func (w gzipResponseWriter) Write(b []byte) (int, error) {
180 return w.WriteCloser.Write(b)
181}
182
183func (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
191type server struct{}
192
193// Any path prefixes that should resolve to index.html.
194var (
Andrew Bonventrea5fff8e2015-11-25 17:04:33 -0500195 fePaths = []string{"/q/", "/c/", "/dashboard/"}
Andrew Bonventreba698352015-11-04 11:14:54 -0500196 issueNumRE = regexp.MustCompile(`^\/\d+\/?$`)
197)
198
199func (_ *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}