func (s *httpService) handle(req *http.Request, sc *httputil.ServerConn, tls, sticky bool) (done bool) { req.Header.Set("X-Request-Start", strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)) req.Header.Set("X-Request-Id", random.UUID()) var backend *httputil.ClientConn var stickyCookie *http.Cookie if sticky { backend, stickyCookie = s.getBackendSticky(req) } else { backend = s.getBackend() } if backend == nil { log.Println("no backend found") fail(sc, req, 503, "Service Unavailable") return } defer backend.Close() req.Proto = "HTTP/1.1" req.ProtoMajor = 1 req.ProtoMinor = 1 delete(req.Header, "Te") delete(req.Header, "Transfer-Encoding") if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { // If we aren't the first proxy retain prior // X-Forwarded-For information as a comma+space // separated list and fold multiple headers into one. if prior, ok := req.Header["X-Forwarded-For"]; ok { clientIP = strings.Join(prior, ", ") + ", " + clientIP } req.Header.Set("X-Forwarded-For", clientIP) } if tls { req.Header.Set("X-Forwarded-Proto", "https") } else { req.Header.Set("X-Forwarded-Proto", "http") } // TODO: Set X-Forwarded-Port // Pass the Request-URI verbatim without any modifications req.URL.Opaque = strings.Split(strings.TrimPrefix(req.RequestURI, req.URL.Scheme+":"), "?")[0] if err := backend.Write(req); err != nil { log.Println("server write err:", err) // TODO: return error to client here return true } res, err := backend.Read(req) if res != nil { if stickyCookie != nil { res.Header.Add("Set-Cookie", stickyCookie.String()) } if res.StatusCode == http.StatusSwitchingProtocols { res.Body = nil } if err := sc.Write(req, res); err != nil { if err != io.EOF && err != httputil.ErrPersistEOF { log.Println("client write err:", err) // TODO: log error } return true } } if err != nil { if err != io.EOF && err != httputil.ErrPersistEOF { log.Println("server read err:", err) // TODO: log error fail(sc, req, 502, "Bad Gateway") } return } // TODO: Proxy HTTP CONNECT? (example: Go RPC over HTTP) if res.StatusCode == http.StatusSwitchingProtocols { serverW, serverR := backend.Hijack() clientW, clientR := sc.Hijack() defer serverW.Close() done := make(chan struct{}) go func() { serverR.WriteTo(clientW) if cw, ok := clientW.(writeCloser); ok { cw.CloseWrite() } close(done) }() clientR.WriteTo(serverW) serverW.(writeCloser).CloseWrite() <-done return true } return }
func (s *httpService) handle(req *http.Request, sc *httputil.ServerConn, tls, sticky bool) { for { req.Header.Set("X-Request-Start", strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)) var backend *httputil.ClientConn var stickyCookie *http.Cookie if sticky { backend, stickyCookie = s.getBackendSticky(req) } else { backend = s.getBackend() } if backend == nil { log.Println("no backend found") fail(sc, req, 503, "Service Unavailable") return } if req.Method != "GET" && req.Method != "POST" && req.Method != "HEAD" && req.Method != "OPTIONS" && req.Method != "PUT" && req.Method != "DELETE" && req.Method != "TRACE" { fail(sc, req, 405, "Method not allowed") return } req.Proto = "HTTP/1.1" req.ProtoMajor = 1 req.ProtoMinor = 1 delete(req.Header, "Te") delete(req.Header, "Transfer-Encoding") if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { // If we aren't the first proxy retain prior // X-Forwarded-For information as a comma+space // separated list and fold multiple headers into one. if prior, ok := req.Header["X-Forwarded-For"]; ok { clientIP = strings.Join(prior, ", ") + ", " + clientIP } req.Header.Set("X-Forwarded-For", clientIP) } if tls { req.Header.Set("X-Forwarded-Proto", "https") } else { req.Header.Set("X-Forwarded-Proto", "http") } // TODO: Set X-Forwarded-Port if err := backend.Write(req); err != nil { log.Println("server write err:", err) return } res, err := backend.Read(req) if res != nil { if stickyCookie != nil { res.Header.Add("Set-Cookie", stickyCookie.String()) } if res.StatusCode == http.StatusSwitchingProtocols { res.Body = nil } if err := sc.Write(req, res); err != nil { if err != io.EOF && err != httputil.ErrPersistEOF { log.Println("client write err:", err) // TODO: log error } return } } if err != nil { if err != io.EOF && err != httputil.ErrPersistEOF { log.Println("server read err:", err) // TODO: log error fail(sc, req, 502, "Bad Gateway") } return } // TODO: Proxy HTTP CONNECT? (example: Go RPC over HTTP) if res.StatusCode == http.StatusSwitchingProtocols { serverW, serverR := backend.Hijack() clientW, clientR := sc.Hijack() defer serverW.Close() done := make(chan struct{}) go func() { serverR.WriteTo(clientW) if cw, ok := clientW.(writeCloser); ok { cw.CloseWrite() } close(done) }() clientR.WriteTo(serverW) serverW.(writeCloser).CloseWrite() <-done return } // close the backend connection, so we don't accidently send to // a closed socket on the backend backend.Close() // TODO: http pipelining req, err = sc.Read() if err != nil { if err != io.EOF && err != httputil.ErrPersistEOF { log.Println("client read err:", err) } return } } }
// function to do the actual testing and output the necessary bits // we don't really care about ordering here otherwise we'd need to do a better job of organizing the output func (w *Worker) testHost(work *Work) { // more debug w.dbg(3, "Ok.. prepping to test %s\n", work.target) // pre-define myConn/tlsConn/client var myConn net.Conn var tlsConn *tls.Conn var client *httputil.ClientConn // need a couple of vars for holding ip addresses and default them to being empty var ip4 string = "" var ip6 string = "" // and a var to stash our full host:port combo in var address string = "" // create a buffer we'll use later for reading client body buffer := make([]byte, 1024) // build our request request, err := http.NewRequest("GET", work.url, nil) if err != nil { w.dbgError("Failed to build request for %s : %s\n", work.url, err) return } if config.ua == "" { request.Header.Set("User-Agent", version) } else { request.Header.Set("User-Agent", config.ua) } // debug w.dbg(3, "Request: %+v\n", request) // set a timer for the connection timestart := time.Now() ipaddr, ierr := net.LookupHost(work.target) if ierr != nil { w.dbgError("Failed to lookup %s : %s", work.target, ierr) return } for _, x := range ipaddr { if len(x) > 16 { if ip6 == "" { ip6 = x } } else if ip4 == "" { ip4 = x } } timestop := time.Now() iplookup := (timestop.Sub(timestart)) // build our address string if config.tcpfour { address = fmt.Sprintf("%s:%d", ip4, work.port) } else { address = fmt.Sprintf("[%s]:%d", ip6, work.port) } // debug w.dbg(1, "Attempting to connect to %s\n", address) // since we want some very low level access to bits and pieces, we're going to have to use tcp dial vs the native http client // create a net.conn w.dbg(1, "Connecting to %s\n", address) if config.tcpfour { myConn, err = net.DialTimeout("tcp4", address, time.Duration(config.timeout)*time.Second) } else if config.tcpsix { myConn, err = net.DialTimeout("tcp6", address, time.Duration(config.timeout)*time.Second) } else { myConn, err = net.DialTimeout("tcp", address, time.Duration(config.timeout)*time.Second) } if err != nil { w.dbgError("Could not connect to %s : %s\n", address, err) return } w.dbg(2, "Connected to %s\n", address) // get a time reading on how long it took to connect to the socket timestop = time.Now() tcpConnect := (timestop.Sub(timestart)) // defer close defer myConn.Close() // need to add some deadlines so we don't sit around indefintely - 5s is more than sufficient myConn.SetDeadline(time.Now().Add(time.Duration(5 * time.Second))) // if we're an ssl connection, we need a few extra steps here if work.ssl { w.dbg(1, "Starting SSL procedures...\n") // default to allowing insecure ssl tlsConfig := tls.Config{InsecureSkipVerify: true} // create a real tls connection tlsConn = tls.Client(myConn, &tlsConfig) // do our SSL negotiation err = tlsConn.Handshake() if err != nil { w.dbgError("Could not negotiate tls handshake on %s : %s\n", address, err) return } // defer closing this connection as well defer tlsConn.Close() } // get a time reading on how long it took to negotiate ssl timestop = time.Now() sslHandshake := (timestop.Sub(timestart)) // get our state if work.ssl { state := tlsConn.ConnectionState() w.dbg(2, "Handshake Complete: %t\n", state.HandshakeComplete) w.dbg(2, "Mutual: %t\n", state.NegotiatedProtocolIsMutual) } // debug w.dbg(3, "Converting to an HTTP client connection...\n") // convert to an http connection if work.ssl { client = httputil.NewProxyClientConn(tlsConn, nil) } else { client = httputil.NewProxyClientConn(myConn, nil) } // debug w.dbg(1, "Making GET request\n") // write our request to the socket err = client.Write(request) if err != nil { w.dbgError("Error writing request : %s\n", err) return } // read our response headers response, err := client.Read(request) if err != nil { // did we get a 400? if response.StatusCode == 400 { w.dbgError("400 response received.. \n") return } // did we get a 404? if response.StatusCode == 404 { w.dbgError("404 response received.. \n") return } // any other error, exit out w.dbgError("Error reading response : %s\n", err) return } w.dbg(1, "Status: %s\n", response.Status) // did we get a response? if len(response.Header) == 0 { w.dbgError("0 length response, something probably broke") return } // measure response header time timestop = time.Now() respTime := (timestop.Sub(timestart)) // defer close since we still want to read the body of the object defer response.Body.Close() // build a reader br := bufio.NewReader(response.Body) // now read the first byte c, err := br.ReadByte() if err != nil { w.dbgError("Could not read data: %s\n", err) return } // measure our first byte time, this is normally 0ms however longer periods could be indicative of a problem timestop = time.Now() byteTime := (timestop.Sub(timestart)) // ok, read the rest of the response n, err := br.Read(buffer) count := n for err != io.EOF { n, err = br.Read(buffer) count += n } // did we fail to read everything? if err != nil && err != io.EOF { w.dbgError("Error on data read, continuing with only %d bytes of %s read\n", count+1, response.Header.Get("Content-Length")) } // measure our overall time to proccess the entire transaction timestop = time.Now() totalTime := (timestop.Sub(timestart)) w.dbg(2, "Received %d bytes total\n", count+1) w.dbg(3, "Response: %s%s\n", string(c), string(buffer)) // shut down the client client.Close() // properly close out our other connections myConn.Close() if work.ssl { tlsConn.Close() } if work.gph != nil { // Graphite selected w.dbg(2, "Graphite selected...\n") gname := strings.Replace(work.target, ".", "_", -1) prefix := "" if config.prefix != "" { prefix = fmt.Sprintf("%s/", config.prefix) } if work.ssl { work.gph.PostOne(fmt.Sprintf("%s%s/ssl_port_%d/dns_time", prefix, gname, work.port), float64(iplookup/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/ssl_port_%d/connect_time", prefix, gname, work.port), float64(tcpConnect/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/ssl_port_%d/ssl_time", prefix, gname, work.port), float64(sslHandshake/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/ssl_port_%d/response_time", prefix, gname, work.port), float64(respTime/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/ssl_port_%d/byte_time", prefix, gname, work.port), float64(byteTime/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/ssl_port_%d/total_time", prefix, gname, work.port), float64(totalTime/1000000)) } else { work.gph.PostOne(fmt.Sprintf("%s%s/http_port_%d/dns_time", prefix, gname, work.port), float64(iplookup/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/http_port_%d/connect_time", prefix, gname, work.port), float64(tcpConnect/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/http_port_%d/response_time", prefix, gname, work.port), float64(respTime/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/http_port_%d/byte_time", prefix, gname, work.port), float64(byteTime/1000000)) work.gph.PostOne(fmt.Sprintf("%s%s/http_port_%d/total_time", prefix, gname, work.port), float64(totalTime/1000000)) } // if we've defined multiple, output in all defined formats + stdout if !config.multiple { return } } // PublishMetric(host string, instance string, key string, value int64) (error Error) { // add tsdb writing if config.tval != "" { w.dbg(2, "Writing to tsdb server...\n") var METRIC []interface{} var tags map[string]interface{} var metric map[string]interface{} tstamp := int64(time.Now().Unix()) if work.ssl { tags = map[string]interface{}{"host": work.target, "port": work.port, "type": "ssl"} metric = map[string]interface{}{"metric": "handshake_time", "value": int64(sslHandshake / 1000000), "timestamp": tstamp, "tags": tags} METRIC = append(METRIC, metric) } else { tags = map[string]interface{}{"Host": work.target, "Port": work.port, "Type": "http"} } metric = map[string]interface{}{"metric": "dns_lookup", "value": int64(iplookup / 1000000), "timestamp": tstamp, "tags": tags} METRIC = append(METRIC, metric) metric = map[string]interface{}{"metric": "connect_time", "value": int64(tcpConnect / 1000000), "timestamp": tstamp, "tags": tags} METRIC = append(METRIC, metric) metric = map[string]interface{}{"metric": "response_time", "value": int64(respTime / 1000000), "timestamp": tstamp, "tags": tags} METRIC = append(METRIC, metric) metric = map[string]interface{}{"metric": "firstbyte_time", "value": int64(byteTime / 1000000), "timestamp": tstamp, "tags": tags} METRIC = append(METRIC, metric) metric = map[string]interface{}{"metric": "total_time", "value": int64(totalTime / 1000000), "timestamp": tstamp, "tags": tags} METRIC = append(METRIC, metric) b, _ := json.Marshal(METRIC) err = writeTSDB(b) if err != nil { dbg(1, "Error writing to tsdb server: %s\n", err) } // if we've defined multiple, output in all defined formats + stdout if !config.multiple { return } } // add collectd style output if config.collectd { if work.ssl { fmt.Fprintf(os.Stdout, "PUTVAL %s/ssl_port_%d/milliseconds-dns_time interval=%d N:%d\n", work.target, work.port, config.interval, iplookup/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/ssl_port_%d/milliseconds-connect_time interval=%d N:%d\n", work.target, work.port, config.interval, tcpConnect/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/ssl_port_%d/milliseconds-ssl_time interval=%d N:%d\n", work.target, work.port, config.interval, sslHandshake/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/ssl_port_%d/milliseconds-response_time interval=%d N:%d\n", work.target, work.port, config.interval, respTime/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/ssl_port_%d/milliseconds-byte_time interval=%d N:%d\n", work.target, work.port, config.interval, byteTime/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/ssl_port_%d/milliseconds-total_time interval=%d N:%d\n", work.target, work.port, config.interval, totalTime/1000000) } else { fmt.Fprintf(os.Stdout, "PUTVAL %s/http_port_%d/milliseconds-dns_time interval=%d N:%d\n", work.target, work.port, config.interval, iplookup/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/http_port_%d/milliseconds-connect_time interval=%d N:%d\n", work.target, work.port, config.interval, tcpConnect/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/http_port_%d/milliseconds-response_time interval=%d N:%d\n", work.target, work.port, config.interval, respTime/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/http_port_%d/milliseconds-byte_time interval=%d N:%d\n", work.target, work.port, config.interval, byteTime/1000000) fmt.Fprintf(os.Stdout, "PUTVAL %s/http_port_%d/milliseconds-total_time interval=%d N:%d\n", work.target, work.port, config.interval, totalTime/1000000) } // if we've defined multiple, output in all defined formats + stdout if !config.multiple { return } } // add function for zabbix if config.zabbix { if work.ssl { fmt.Fprintf(os.Stdout, "https_host=%s, port=%d, dns_lookup=%dms, socket_connect=%dms, ssl_negotiation=%dms, response_time=%dms, first_byte=%dms, total_time=%dms ", work.target, work.port, iplookup/1000000, tcpConnect/1000000, sslHandshake/1000000, respTime/1000000, byteTime/1000000, totalTime/1000000) if config.expires { i := 0 state := tlsConn.ConnectionState() for _, v := range state.PeerCertificates { if i == 0 { myT := time.Now() myExpires := v.NotAfter.Sub(myT) fmt.Fprintf(os.Stdout, "expires=%dd\n", myExpires/8.64e13) i++ } } } else { fmt.Fprintf(os.Stdout, "\n") } } else { fmt.Fprintf(os.Stdout, "host=%s, port=%d, dns_lookup=%dms, socket_connect=%dms, response_time=%dms, first_byte=%dms, total_time=%dms\n", work.target, work.port, iplookup/1000000, tcpConnect/1000000, respTime/1000000, byteTime/1000000, totalTime/1000000) } // if we've defined multiple, output in all defined formats + stdout if !config.multiple { return } } // print out our values if (!config.multiple) || (config.verbose) { if work.ssl { log.Printf("Host: %s -> DNS Lookup: %dms, Socket Connect: %dms, SSL Negotiation: %dms, Response Time: %dms, 1st Byte: %dms, Total Time: %dms\n", work.target, iplookup/1000000, tcpConnect/1000000, sslHandshake/1000000, respTime/1000000, byteTime/1000000, totalTime/1000000) } else { log.Printf("Host: %s -> DNS Lookup: %dms, Socket Connect: %dms, Response Time: %dms, 1st Byte: %dms, Total Time: %dms\n", work.target, iplookup/1000000, tcpConnect/1000000, respTime/1000000, byteTime/1000000, totalTime/1000000) } if work.ssl { if config.expires == true { i := 0 state := tlsConn.ConnectionState() for _, v := range state.PeerCertificates { if i == 0 { myT := time.Now() myExpires := v.NotAfter.Sub(myT) fmt.Fprintf(os.Stdout, "Cert expires in: %d days\n", myExpires/8.64e13) i++ } } } } } if config.verbose { log.Printf("StatusCode: %d\n", response.StatusCode) log.Printf("ProtoCol: %s\n", response.Proto) for k, v := range response.Header { log.Printf("%s: %v\n", k, v) } if work.ssl { if config.printcert == true { i := 0 state := tlsConn.ConnectionState() for _, v := range state.PeerCertificates { if i == 0 { sslFrom := v.NotBefore sslTo := v.NotAfter log.Printf("Server key information:") log.Printf("\tCN:\t%v\n\tOU:\t%v\n\tOrg:\t%v\n", v.Subject.CommonName, v.Subject.OrganizationalUnit, v.Subject.Organization) log.Printf("\tCity:\t%v\n\tState:\t%v\n\tCountry:%v\n", v.Subject.Locality, v.Subject.Province, v.Subject.Country) log.Printf("SSL Certificate Valid:\n\tFrom: %v\n\tTo: %v\n", sslFrom, sslTo) log.Printf("Valid Certificate DNS:\n") if len(v.DNSNames) >= 1 { for dns := range v.DNSNames { log.Printf("\t%v\n", v.DNSNames[dns]) } } else { log.Printf("\t%v\n", v.Subject.CommonName) } i++ } else if i == 1 { log.Printf("Issued by:\n\t%v\n\t%v\n\t%v\n", v.Subject.CommonName, v.Subject.OrganizationalUnit, v.Subject.Organization) i++ } else { // we're done here, lets move on break } } } } // throw in a new line to pretty it up log.Printf("") } return }