diff --git a/README.md b/README.md index 71a1714..7455042 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ $ curl ifconfig.co/port/80 { "ip": "127.0.0.1", "port": 80, - "reachable": false + "reachable": false, + "status": "refused" } ``` diff --git a/http/http.go b/http/http.go index a6bcb5f..712b981 100644 --- a/http/http.go +++ b/http/http.go @@ -2,12 +2,15 @@ package http import ( "encoding/json" + "errors" "fmt" "html/template" "io" "log" + "os" "path/filepath" "strings" + "syscall" "net/http/pprof" @@ -57,10 +60,21 @@ type Response struct { UserAgent *useragent.UserAgent `json:"user_agent,omitempty"` } +type PortStatus string + +const ( + PortOpen PortStatus = "open" // TCP handshake completed + PortRefused PortStatus = "refused" // RST (nothing listening) + PortTimeout PortStatus = "timeout" // Packet dropped + PortUnreachable PortStatus = "unreachable" // Host/Network Unreachable + PortUnknown PortStatus = "unknown" // Unhandled error +) + type PortResponse struct { - IP net.IP `json:"ip"` - Port uint64 `json:"port"` - Reachable bool `json:"reachable"` + IP net.IP `json:"ip"` + Port uint64 `json:"port"` + Reachable bool `json:"reachable"` + Status PortStatus `json:"status"` // Strict enum } func New(db geo.Reader, cache *Cache, profile bool) *Server { @@ -185,10 +199,26 @@ func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) { return PortResponse{Port: port}, err } err = s.LookupPort(ip, port) + + status := PortUnknown + if err == nil { + status = PortOpen + } else { + switch { + case os.IsTimeout(err): + status = PortTimeout + case errors.Is(err, syscall.ECONNREFUSED): + status = PortRefused + case errors.Is(err, syscall.EHOSTUNREACH) || errors.Is(err, syscall.ENETUNREACH): + status = PortUnreachable + } + } + return PortResponse{ IP: ip, Port: port, Reachable: err == nil, + Status: status, }, nil } diff --git a/http/http_test.go b/http/http_test.go index 458653d..ecd537a 100644 --- a/http/http_test.go +++ b/http/http_test.go @@ -165,9 +165,9 @@ func TestJSONHandlers(t *testing.T) { {s.URL + "/port/foo", "{\n \"status\": 400,\n \"error\": \"invalid port: foo\"\n}", 400}, {s.URL + "/port/0", "{\n \"status\": 400,\n \"error\": \"invalid port: 0\"\n}", 400}, {s.URL + "/port/65537", "{\n \"status\": 400,\n \"error\": \"invalid port: 65537\"\n}", 400}, - {s.URL + "/port/31337", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 31337,\n \"reachable\": true\n}", 200}, - {s.URL + "/port/80", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true\n}", 200}, // checking that our test server is reachable on port 80 - {s.URL + "/port/80?ip=1.3.3.7", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true\n}", 200}, // ensuring that the "ip" parameter is not usable to check remote host ports + {s.URL + "/port/31337", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 31337,\n \"reachable\": true,\n \"status\": \"open\"\n}", 200}, + {s.URL + "/port/80", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true,\n \"status\": \"open\"\n}", 200}, // checking that our test server is reachable on port 80 + {s.URL + "/port/80?ip=1.3.3.7", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true,\n \"status\": \"open\"\n}", 200}, // ensuring that the "ip" parameter is not usable to check remote host ports {s.URL + "/foo", "{\n \"status\": 404,\n \"error\": \"404 page not found\"\n}", 404}, {s.URL + "/health", `{"status":"OK"}`, 200}, }