├── .godir ├── Procfile ├── README.md ├── use_proxy.go └── apiproxy ├── apiproxy.go └── apiproxy_test.go /.godir: -------------------------------------------------------------------------------- 1 | use_proxy 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: use_proxy 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | simpleapiproxy 2 | ============ 3 | 4 | A small proxy to allow consuming apis via javascript without exposing the api keys. 5 | 6 | As an example, I'm using this with last.fm for a testbed. 7 | 8 | example config: 9 | 10 | URL_ROOT=http://ws.audioscrobbler.com/2.0/ URL_SUFFIX=api_key=XXXXXXXXXXXXX PORT=80 11 | 12 | This would serve http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=violencenow&format=json&api_key=XXXXXXXXXXXXX when you request http://example.com/?method=user.getrecenttracks&user=violencenow&format=json 13 | 14 | deploying 15 | ============ 16 | 17 | Just folow the instructions for adding the heroku buildpack and you should be good to go: https://github.com/kr/heroku-buildpack-go 18 | 19 | Procfile included. 20 | -------------------------------------------------------------------------------- /use_proxy.go: -------------------------------------------------------------------------------- 1 | // # Api Proxy 2 | // 3 | // ## Purpose: 4 | // Provides a fast, intermediate server to conceal your api keys when proxying 5 | // to an api 6 | // 7 | // ## Inputs: 8 | // 1. PORT: which port to run on ( e.g. 8080 ) 9 | // 2. URL_ROOT: the destination root url ( e.g. http://foo.com/api/ ) 10 | // 3. URL_SUFFIX: the query params you wish to transparently append to the each 11 | // request. ( e.g. a suffix of "language=en&key=XXXXXX" would result in the 12 | // url http://foo.com/test?name=bob being requested transparently as 13 | // http://foo.com/test?name=bob&language=en&key=XXXXXX ) 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/semanticart/simpleapiproxy/apiproxy" 19 | "net/http" 20 | "os" 21 | ) 22 | 23 | func main() { 24 | apiProxy := apiproxy.Proxy(os.Getenv("URL_ROOT"), os.Getenv("URL_SUFFIX")) 25 | http.ListenAndServe(":"+os.Getenv("PORT"), apiProxy) 26 | } 27 | -------------------------------------------------------------------------------- /apiproxy/apiproxy.go: -------------------------------------------------------------------------------- 1 | package apiproxy 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | ) 9 | 10 | // Set the proxied request's host to the destination host (instead of the 11 | // source host). e.g. http://foo.com proxying to http://bar.com will ensure 12 | // that the proxied requests appear to be coming from http://bar.com 13 | // 14 | // For both this function and queryCombiner (below), we'll be wrapping a 15 | // Handler with our own HandlerFunc so that we can do some intermediate work 16 | func sameHost(handler http.Handler) http.Handler { 17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 | r.Host = r.URL.Host 19 | handler.ServeHTTP(w, r) 20 | }) 21 | } 22 | 23 | // Append additional query params to the original URL query. 24 | func queryCombiner(handler http.Handler, addon string) http.Handler { 25 | // first parse the provided string to pull out the keys and values 26 | values, err := url.ParseQuery(addon) 27 | if err != nil { 28 | log.Fatal("addon failed to parse") 29 | } 30 | 31 | // now we apply our addon params to the existing query 32 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | query := r.URL.Query() 34 | 35 | for k, _ := range values { 36 | query.Add(k, values.Get(k)) 37 | } 38 | 39 | r.URL.RawQuery = query.Encode() 40 | handler.ServeHTTP(w, r) 41 | }) 42 | } 43 | 44 | // Allow cross origin resource sharing 45 | func addCORS(handler http.Handler) http.Handler { 46 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 | w.Header().Set("Access-Control-Allow-Origin", "*") 48 | w.Header().Set("Access-Control-Allow-Headers", "X-Requested-With") 49 | handler.ServeHTTP(w, r) 50 | }) 51 | } 52 | 53 | // Combine the two functions above with http.NewSingleHostReverseProxy 54 | func Proxy(remoteUrl string, queryAddon string) http.Handler { 55 | // pull the root url we're proxying to from an environment variable. 56 | serverUrl, err := url.Parse(remoteUrl) 57 | if err != nil { 58 | log.Fatal("URL failed to parse") 59 | } 60 | 61 | // initialize our reverse proxy 62 | reverseProxy := httputil.NewSingleHostReverseProxy(serverUrl) 63 | // wrap that proxy with our sameHost function 64 | singleHosted := sameHost(reverseProxy) 65 | // wrap that with our query param combiner 66 | combined := queryCombiner(singleHosted, queryAddon) 67 | // and finally allow CORS 68 | return addCORS(combined) 69 | } 70 | -------------------------------------------------------------------------------- /apiproxy/apiproxy_test.go: -------------------------------------------------------------------------------- 1 | package apiproxy 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/http/httputil" 10 | "net/url" 11 | "testing" 12 | ) 13 | 14 | func TestSameHost(t *testing.T) { 15 | destinationServer := httptest.NewServer(http.HandlerFunc(hostDumper)) 16 | destinationHost := hostFor(destinationServer) 17 | 18 | reverseProxy := reverseProxyFor(destinationServer) 19 | defaultReverseProxyHost := hostFor(httptest.NewServer(reverseProxy)) 20 | 21 | if defaultReverseProxyHost == destinationHost { 22 | t.Errorf("expected reverseProxy host %s to not equal destinationServer host %s", defaultReverseProxyHost, destinationHost) 23 | } 24 | 25 | correctedHost := hostFor(httptest.NewServer(sameHost(reverseProxy))) 26 | if correctedHost != destinationHost { 27 | t.Errorf("expected correctedHost %s to equal destinationServer host %s", correctedHost, destinationHost) 28 | } 29 | } 30 | 31 | func TestQueryCombiner(t *testing.T) { 32 | initialQuery := "q=some-test&x=y" 33 | toAdd := "key=1234&name=bob" 34 | expectedModified := toAdd + "&" + initialQuery 35 | 36 | destinationServer := httptest.NewServer(http.HandlerFunc(queryDumper)) 37 | if queryFor(destinationServer, initialQuery) != initialQuery { 38 | t.Errorf("expected initial query %s to be unmodified but was %s", initialQuery, queryFor(destinationServer, initialQuery)) 39 | } 40 | 41 | modifiedServer := httptest.NewServer(queryCombiner(http.HandlerFunc(queryDumper), toAdd)) 42 | modifiedQuery := queryFor(modifiedServer, initialQuery) 43 | if modifiedQuery != expectedModified { 44 | t.Errorf("expected %s after modification but was %s", expectedModified, modifiedQuery) 45 | } 46 | } 47 | 48 | func hostFor(s *httptest.Server) string { 49 | resp, err := http.Get(s.URL) 50 | return readBody(resp, err) 51 | } 52 | 53 | func readBody(resp *http.Response, err error) string { 54 | content, err := ioutil.ReadAll(resp.Body) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | resp.Body.Close() 60 | 61 | return string(content[:]) 62 | } 63 | 64 | func queryFor(s *httptest.Server, baseQuery string) string { 65 | resp, err := http.Get(s.URL + "?" + baseQuery) 66 | return readBody(resp, err) 67 | } 68 | 69 | func reverseProxyFor(server *httptest.Server) *httputil.ReverseProxy { 70 | serverUrl, err := url.Parse(server.URL) 71 | if err != nil { 72 | log.Fatal("URL failed to parse") 73 | } 74 | return httputil.NewSingleHostReverseProxy(serverUrl) 75 | } 76 | 77 | func hostDumper(w http.ResponseWriter, r *http.Request) { 78 | fmt.Fprintf(w, r.Host) 79 | } 80 | 81 | func queryDumper(w http.ResponseWriter, r *http.Request) { 82 | fmt.Fprintf(w, r.URL.RawQuery) 83 | } 84 | --------------------------------------------------------------------------------