diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..722935b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +eznet diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ff49e4 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# eznetstat: A command line application with netstat shortcuts + +Have you ever needed to remember how to check what process is on a port and forgotten how to do it? There are loads of netstat commands that can be tricky to remember. This is a work in progress command line application to give direct shortcuts to those netstat operations. + +## Options + +### -v, --version +Show application version. + +### -h, --help +Show help menu. + +### -c PORT, --check-port=PORT +Get the name and process ID of the process running on a given port + +## TODO +* Add the ability to kill process on port +* Come up with other useful stuff +* Write up contributors guide +* Decide how to do documentation (probably Github wiki?) +* Write man page (figure out how to do that lol) + +## Contributing +I don't know that I'm ready for contributors yet because I don't have a good contributors guide. I do plan to eventually open this up to contributors. If you have any ideas for future operations, please feel free to open an issue, I am happy to discuss ideas! diff --git a/ltnp/ltnp_operator.cr b/ltnp/ltnp_operator.cr new file mode 100644 index 0000000..228708c --- /dev/null +++ b/ltnp/ltnp_operator.cr @@ -0,0 +1,18 @@ +require "./ltnp_record" + +class LtnpOperator + def initialize(cmd_output : String) + records = cmd_output.each_line + records = records.each.select(/^tcp/) + @ltnp_records = Array(LtnpRecord).new + records.each { |s| @ltnp_records << LtnpRecord.new s } + end + + def say_hi + @ltnp_records.each { |r| puts r.to_s } + end + + def get_port_record(port : String) : LtnpRecord | Nil + @ltnp_records.find { |r| r.port == port } + end +end diff --git a/ltnp/ltnp_record.cr b/ltnp/ltnp_record.cr new file mode 100644 index 0000000..1cf4f45 --- /dev/null +++ b/ltnp/ltnp_record.cr @@ -0,0 +1,53 @@ +require "../modules/record_helpers" + +class LtnpRecord + getter proto : String + getter state : String + getter address : String + getter port : String + getter f_address : String + getter f_port : String + getter state : String + getter pid : String + getter p_name : String + + def initialize(record : String) + ser_record = RecordHelpers.clean_record(record) + @proto = ser_record[0] + @address, @port = ser_address_port ser_record[3].split(':') + @f_address, @f_port = ser_address_port ser_record[4].split(':') + @state = ser_record[5] + @pid, @p_name = ser_pid_p_name ser_record[6] + end + + def to_s + puts "%s, " * 8 % [ + @proto, @state, @address, @port, + @f_address, @f_port, @pid, @p_name + ] + end + + def ser_address_port(address_port : Array) : {String, String} + address = String.new + port = String.new + unless address_port.size > 2 + address = address_port[0] + port = address_port[1] + else + port = address_port[-1] + end + {address, port} + end + + def ser_pid_p_name(pid_pro_str : String) : {String, String} + pid = String.new + p_name = String.new + unless pid_pro_str == "-" + pid_pro = pid_pro_str.split('/') + pid = pid_pro[0] + p_name = pid_pro[1] + end + {pid, p_name} + end + +end diff --git a/main.cr b/main.cr new file mode 100644 index 0000000..936ac42 --- /dev/null +++ b/main.cr @@ -0,0 +1,34 @@ +require "option_parser" +require "./ltnp/ltnp_operator" +require "./modules/netstat_runner" + +OptionParser.parse do |parser| + parser.banner = "Welcome to eznetstat!" + + parser.on "-v", "--version", "Show version" do + puts "0.1" + exit + end + + parser.on "-h", "--help", "Show help" do + puts parser + exit + end + + parser.on "-c PORT", "--check-port=PORT", "Get the process name and ID of process on port" do |port| + ltnp_operator = NetstatRunner.run_ltnp + record = ltnp_operator.get_port_record port + unless record.nil? + puts "Port: %s, Process: %s" % [record.port, record.p_name] + else + puts "No processes found on port %s" % port + end + exit + end + + parser.on "-k", "--kill-port", "Kill process on port" do + ltnp_operator = NetstatRunner.run_ltnp + ltnp_operator.say_hi + exit + end +end diff --git a/modules/netstat_runner.cr b/modules/netstat_runner.cr new file mode 100644 index 0000000..ead6a9c --- /dev/null +++ b/modules/netstat_runner.cr @@ -0,0 +1,7 @@ +module NetstatRunner + def self.run_ltnp() : LtnpOperator + io = IO::Memory.new + Process.run("sudo netstat -ltnp", shell: true, output: io) + LtnpOperator.new io.to_s + end +end diff --git a/modules/record_helpers.cr b/modules/record_helpers.cr new file mode 100644 index 0000000..aa187b2 --- /dev/null +++ b/modules/record_helpers.cr @@ -0,0 +1,7 @@ +module RecordHelpers + def self.clean_record(record : String) : Array(String) + tokenized_record = record.split(" ") + tokenized_record = tokenized_record.reject { |s| s == "" } + tokenized_record + end +end diff --git a/spec/input/ltnp_example.txt b/spec/input/ltnp_example.txt new file mode 100644 index 0000000..418b75d --- /dev/null +++ b/spec/input/ltnp_example.txt @@ -0,0 +1,8 @@ +Active Internet connections (only servers) +Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name +tcp 0 0 127.0.0.1:8081 0.0.0.0:* LISTEN 166475/polymer +tcp 0 0 0.0.0.0:57621 0.0.0.0:* LISTEN 163077/spotify --fo +tcp 0 0 192.168.122.1:53 0.0.0.0:* LISTEN 1286/dnsmasq +tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 35841/sshd +tcp 0 0 0.0.0.0:53323 0.0.0.0:* LISTEN 163077/spotify --fo +tcp6 0 0 :::22 :::* LISTEN 35841/sshd diff --git a/spec/ltnp/ltnp_operator_spec.cr b/spec/ltnp/ltnp_operator_spec.cr new file mode 100644 index 0000000..10ceffe --- /dev/null +++ b/spec/ltnp/ltnp_operator_spec.cr @@ -0,0 +1,19 @@ +require "spec" +require "../../ltnp/ltnp_operator.cr" + +describe LtnpOperator do + sut = LtnpOperator.new File.read("./spec/input/ltnp_example.txt") + + describe "#get_port_record" do + existing_port = "8081" + nonsense_port = "1234" + + it "should find record when port exists" do + sut.get_port_record(existing_port).nil?.should be_false + end + + it "should not find record when port doesn't exist" do + sut.get_port_record(nonsense_port).nil?.should be_true + end + end +end diff --git a/spec/ltnp/ltnp_record_spec.cr b/spec/ltnp/ltnp_record_spec.cr new file mode 100644 index 0000000..79f29f3 --- /dev/null +++ b/spec/ltnp/ltnp_record_spec.cr @@ -0,0 +1,29 @@ +require "spec" +require "../../ltnp/ltnp_record.cr" + +describe LtnpRecord do + sut_normal = LtnpRecord.new "tcp 0 0 127.0.0.1:8081 0.0.0.0:* LISTEN 69/myproc" + sut_address = LtnpRecord.new "tcp 0 0 :::8081 0.0.0.0:* LISTEN 69/myproc" + sut_process = LtnpRecord.new "tcp 0 0 :::8081 0.0.0.0:* LISTEN -" + + it "should have expected properties under normal circumstance" do + sut_normal.proto.should eq("tcp") + sut_normal.address.should eq("127.0.0.1") + sut_normal.port.should eq("8081") + sut_normal.f_address.should eq("0.0.0.0") + sut_normal.f_port.should eq("*") + sut_normal.state.should eq("LISTEN") + sut_normal.pid.should eq("69") + sut_normal.p_name.should eq("myproc") + end + + it "should have expected properties when Local Address column is edge case" do + sut_address.address.empty?.should be_true + sut_address.port.should eq("8081") + end + + it "should have expected properties when PID/ProcessName column is edge case" do + sut_process.pid.empty?.should be_true + sut_process.p_name.empty?.should be_true + end +end