Skip to content
Browse files

initial commit, do not use this code, you've been warned

  • Loading branch information...
0 parents commit a21b3162bf49e2033fb8e1f2db920821f7c3de29 @mattetti committed May 22, 2011
Showing with 374 additions and 0 deletions.
  1. +27 −0 README.md
  2. +53 −0 Rakefile
  3. +38 −0 chunked_writer.go
  4. +112 −0 http_client.go
  5. +30 −0 main.go
  6. +114 −0 route.go
27 README.md
@@ -0,0 +1,27 @@
+# Go Experiment
+
+## Compile
+
+You can easily compile this experiment using Rake, the Ruby version of Make.
+If you have Ruby installed, Rake is available on your machine, just run the following command:
+
+ $ rake compile
+
+You can force the architecture to compile for by setting the ARCH environment.
+Currently the following ARCH values are supported:
+
+* x86-64
+* 386
+* arm
+
+## App
+
+The app doesn't do much yet. It's mainly a way for me to practice Go and try to build something
+a bit more complex than a "Hello World" app.
+
+At this point, the app proxies localhost requests to google.
+Eventually, the app would proxy based on various route rules.
+
+## Dependencies
+
+There are no external dependencies outside of #golang obviously.
53 Rakefile
@@ -0,0 +1,53 @@
+# Compile the code, passing an argument to the task will set the file output.
+# the ARCH environemnt variable to force the arch. currently
+# x86-64 (default), 386 and arm are supported.
+#
+desc "Compile the program, set ARCH to force the architecture."
+task :compile do |task, args|
+ files = %W{chunked_writer.go http_client.go route.go main.go}
+
+ # default architecture is x86-64
+ arch = ENV["ARCH"] || "x86-64"
+ output = (args.first || File.basename(File.dirname(__FILE__))) + "-#{arch}"
+ # Compile
+ err = `#{compiler(arch)} -o #{output}.6 #{files.join(' ')}`
+ raise "Compilation failed: #{err}" unless err == ""
+ # Link
+ err = `#{linker(arch)} -o #{output}.out #{output}.6`
+ raise "Linking failed: #{err}" unless err == ""
+
+ puts "compiled to #{output}.out"
+end
+
+desc "Remove intermediate files (*.6, *.8, *.5)"
+task :cleanup do
+ require 'fileutils'
+ Dir.glob("*.{5,6,8}").each do |f|
+ puts "Deleting #{f}"
+ FileUtils.rm(f)
+ end
+end
+
+
+private
+
+def compiler(arch)
+ compiler_code(arch) + "g"
+end
+
+def linker(arch)
+ compiler_code(arch) + "l"
+end
+
+def compiler_code(arch)
+ case arch
+ when /x86-64/i
+ "6"
+ when /386/
+ "8"
+ when /arm/i
+ "5"
+ else
+ raise "Architecture (#{arch}) not currently supported."
+ end
+end
38 chunked_writer.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "io"
+ "os"
+)
+
+// There is probably already something doing that in a std package
+// but oh well, i'm learning :)
+
+//
+type ChunkedWriter struct {
+ Wire io.Writer
+}
+
+// implement the Writer interface
+func (cw *ChunkedWriter) Write(data []byte) (n int, err os.Error) {
+ // Don't send 0-length data. It looks like EOF for chunked encoding.
+ if len(data) == 0 {
+ return 0, nil
+ }
+
+ if n, err = cw.Wire.Write(data); err != nil {
+ return
+ }
+ if n != len(data) {
+ err = io.ErrShortWrite
+ return
+ }
+
+ return
+}
+
+// implement the Writer interface
+func (cw *ChunkedWriter) Close() os.Error {
+ _, err := io.WriteString(cw.Wire, "0\r\n")
+ return err
+}
112 http_client.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "http"
+ "os"
+ "log"
+ "fmt"
+)
+
+
+type HttpClient struct {
+ client http.Client
+}
+
+// Forward a request to another domain
+// By design Go doesn't let you define new methods on non-local types
+func (this *HttpClient) Forward(req *http.Request, newDomain string) (resp *http.Response, err os.Error){
+
+ log.Printf("req host: %v\n", req.Host)
+ // TODO change the the domain back
+ for _, cookie := range req.Cookie {
+ log.Printf("req cookie: %v\n", cookie)
+ }
+
+
+ var base *http.URL
+ /*var r *http.Response*/
+ url := (newDomain + req.URL.RawPath)
+ log.Printf("forwarding to: " + url)
+
+ redirectChecker := this.client.CheckRedirect
+ if redirectChecker == nil {
+ redirectChecker = defaultCheckRedirect
+ }
+ var via []*http.Request
+
+ for redirect := 0; ; redirect++ {
+ var nReq = http.Request{}
+ nReq.Method = req.Method
+ //log.Printf("HTTP method: " + nReq.Method)
+ nReq.Header = req.Header
+ //log.Printf("Headers: %v\n", nReq.Header)
+
+ if base == nil {
+ nReq.URL, err = http.ParseURL(url)
+ } else {
+ nReq.URL, err = base.ParseURL(url)
+ }
+
+ if err != nil {
+ break
+ }
+
+ // Set the redirection referer headers
+ if len(via) > 0 {
+ // Add the Referer header.
+ lastReq := via[len(via)-1]
+ if lastReq.URL.Scheme != "https" {
+ nReq.Referer = lastReq.URL.String()
+ }
+
+ err = redirectChecker(&nReq, via)
+ if err != nil {
+ break
+ }
+ }
+
+ url = nReq.URL.String()
+ // Wrapped the client so I could do that ...sigh...
+ // Also, #Do i a wrapper around #send
+ if resp, err = this.client.Do(&nReq); err != nil {
+ break
+ }
+
+ if shouldRedirect(resp.StatusCode) {
+ resp.Body.Close()
+ if url = resp.Header.Get("Location"); url == "" {
+ err = os.ErrorString(fmt.Sprintf("%d response missing Location header", resp.StatusCode))
+ break
+ }
+ base = req.URL
+ via = append(via, &nReq)
+ continue
+ }
+ // log.Printf("final URL: " + url)
+
+ return
+ }
+
+ err = &http.URLError{req.Method, url, err}
+ return
+}
+
+
+// copied over from the http package
+func defaultCheckRedirect(req *http.Request, via []*http.Request) os.Error {
+ if len(via) >= 10 {
+ return os.ErrorString("stopped after 10 redirects")
+ }
+ return nil
+}
+
+// copied over from the http package
+// True if the specified HTTP status code is one for which the Get utility should
+// automatically redirect.
+func shouldRedirect(statusCode int) bool {
+ switch statusCode {
+ case http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther, http.StatusTemporaryRedirect:
+ return true
+ }
+ return false
+}
30 main.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "http"
+ "log"
+)
+
+// TODO: the uri removal from namespaced domains doesn't work yet
+func main(){
+ // Define the routes
+ routes := []*Route{ &Route{ uri: "/",
+ endpoint: "http://google.com",
+ domain: "google.com",
+ client: HttpClient{client: http.Client{}} },
+ &Route{ uri: "/yahoo",
+ endpoint: "http://yahoo.com",
+ domain: "yahoo.com",
+ client: HttpClient{client: http.Client{}}} }
+
+ // Set the route handlers
+ for _, route := range routes {
+ log.Printf("Handling base route: " + route.uri)
+ http.Handle(route.uri, route)
+ }
+
+ // Start the server
+ log.Printf("Starting server on http://127.0.0.1:10980")
+ err := http.ListenAndServe(":10980", nil)
+ if err != nil { log.Fatal(err) }
+}
114 route.go
@@ -0,0 +1,114 @@
+package main
+
+import(
+ "log"
+ "http"
+ "io"
+ "regexp"
+ //"os"
+)
+
+type Route struct {
+ uri string
+ endpoint string
+ domain string
+ client HttpClient
+}
+
+
+// Implementing the Handler interface
+func (r *Route) ServeHTTP(w http.ResponseWriter, req *http.Request){
+ // http.Redirect(w, req, r.endpoint, 302)
+
+ // Proxy the request
+ // should use a connection pool and reuse connections
+ //if err != nil { log.Fatal(err) }
+
+ pResponse, err := r.client.Forward(req, r.endpoint)
+ /*log.Printf("forwarded response: %v\n", pResponse)*/
+ if err != nil { log.Fatal(err) }
+ if pResponse == nil{
+ log.Printf("Empty response")
+ } else {
+ //pResponse, err := client.Do(pReq)
+ defer pResponse.Body.Close()
+ if err != nil { log.Fatal(err) }
+
+ // Convert the cookies
+ // TODO: convert to the domain saw by the client
+ for _, cookie := range pResponse.SetCookie {
+ rexp, err := regexp.Compile("domain=." + r.domain)
+ if err != nil { log.Fatal(err) }
+ newCookie := rexp.ReplaceAllString(cookie.Raw, "domain=127.0.0.1")
+ w.Header().Add("Set-Cookie", newCookie)
+ log.Printf("Modified Cookie: " + newCookie)
+ }
+
+ log.Printf("Proxied url: " + r.endpoint + req.URL.RawPath + "\n\n")
+
+
+ // push back to the client
+ cw := &ChunkedWriter{w}
+ _, err = io.Copy(cw, pResponse.Body)
+ if err == nil { err = cw.Close() }
+ }
+}
+
+
+func (r *Route) newDispatcher(){
+ log.Printf("uri: " + r.uri + " endpoint: " + r.endpoint)
+ http.Handle(r.uri, r)
+}
+
+
+/*
+// Could have also been implemented like that.
+// Where we dynamically create a dynamic handler and use that.
+
+// in main
+for _, value := range routes {
+ value.newDispatcher()
+}
+
+// then here
+
+func (r *Route) newDispatcher(){
+ log.Printf("uri: " + r.uri + " endpoint: " + r.endpoint)
+ http.Handle(r.uri, r)
+}
+
+func (r *Route) newDispatcher(){
+ log.Printf("uri: " + r.uri + " endpoint: " + r.endpoint)
+
+ dynHandler := func(w http.ResponseWriter, req *http.Request){
+ // Reimplementing http://golang.org/pkg/http/#ReverseProxy
+ // http.Redirect(w, req, r.endpoint, 302)
+
+ // Proxy the request
+ client := http.Client{}
+ pResponse, url, err := client.Get(r.endpoint + req.URL.RawPath)
+ if err != nil { log.Fatal(err) }
+
+ // copy the headers
+ for key, value := range pResponse.Header {
+ log.Printf(key + " : " + value[0])
+ if key == "Cookie" {
+ w.Header().Set("Set-Cookie", value[0])
+ } else {
+ w.Header().Set(key, value[0])
+ }
+ }
+
+ log.Printf("Proxied url: " + url)
+ defer pResponse.Body.Close()
+
+ cw := &CustomChunkedWriter{w}
+ _, err = io.Copy(cw, pResponse.Body)
+ if err == nil { err = cw.Close() }
+ }
+
+ http.HandleFunc(r.uri, dynHandler)
+}
+*/
+
+

0 comments on commit a21b316

Please sign in to comment.
Something went wrong with that request. Please try again.