Skip to content

Commit 9b80d9d

Browse files
committed
TLS ping host and port. 🔑
0 parents  commit 9b80d9d

16 files changed

+688
-0
lines changed

.gitignore

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Created by https://www.toptal.com/developers/gitignore/api/ruby
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=ruby
3+
4+
### Ruby ###
5+
*.gem
6+
*.rbc
7+
/.config
8+
/coverage/
9+
/InstalledFiles
10+
/pkg/
11+
/spec/reports/
12+
/spec/examples.txt
13+
/test/tmp/
14+
/test/version_tmp/
15+
/tmp/
16+
17+
# Used by dotenv library to load environment variables.
18+
# .env
19+
20+
# Ignore Byebug command history file.
21+
.byebug_history
22+
23+
## Specific to RubyMotion:
24+
.dat*
25+
.repl_history
26+
build/
27+
*.bridgesupport
28+
build-iPhoneOS/
29+
build-iPhoneSimulator/
30+
31+
## Specific to RubyMotion (use of CocoaPods):
32+
#
33+
# We recommend against adding the Pods directory to your .gitignore. However
34+
# you should judge for yourself, the pros and cons are mentioned at:
35+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
36+
# vendor/Pods/
37+
38+
## Documentation cache and generated files:
39+
/.yardoc/
40+
/_yardoc/
41+
/doc/
42+
/rdoc/
43+
44+
## Environment normalization:
45+
/.bundle/
46+
/vendor/bundle
47+
/lib/bundler/man/
48+
49+
# for a library or gem, you might want to ignore these files since the code is
50+
# intended to run in multiple environments; otherwise, check them in:
51+
Gemfile.lock
52+
.ruby-version
53+
.ruby-gemset
54+
55+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
56+
.rvmrc
57+
58+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
59+
# .rubocop-https?--*
60+
61+
# End of https://www.toptal.com/developers/gitignore/api/ruby

.rspec

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--require spec_helper

.rubocop.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
require:
3+
- rubocop-rspec
4+
- rubocop-rake
5+
- rubocop-performance
6+
7+
AllCops:
8+
NewCops: enable
9+
10+
Style/NegatedIf:
11+
Enabled: false
12+
13+
Metrics/MethodLength:
14+
CountAsOne:
15+
- 'array'
16+
- 'hash'
17+
- 'heredoc'
18+
Max: 25
19+
20+
Metrics/AbcSize:
21+
CountRepeatedAttributes: false
22+
23+
Style/Documentation:
24+
Enabled: false

Gemfile

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
source 'https://rubygems.org'
4+
gemspec
5+
6+
group :development, :test do
7+
gem 'rake'
8+
9+
gem 'pry'
10+
gem 'pry-byebug'
11+
gem 'pry-doc'
12+
gem 'pry-rescue'
13+
14+
gem 'rspec'
15+
16+
gem 'rubocop'
17+
gem 'rubocop-performance'
18+
gem 'rubocop-rake'
19+
gem 'rubocop-rspec'
20+
21+
gem 'simplecov'
22+
end

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Tobias Schäfer
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# tls-ping
2+
3+
TLS ping host and port.
4+
5+
## Introduction
6+
7+
**tls-ping** connects to a given host and port and validates the TLS connection
8+
and certificate.
9+
10+
## Installation
11+
12+
```bash
13+
gem build
14+
version=$(ruby -Ilib -e 'require "tls/ping"; puts TLS::Ping::VERSION')
15+
gem install tls-ping-${version}.gem
16+
```
17+
18+
## Usage
19+
20+
```bash
21+
$ tls-ping github.com 443
22+
> github.com:443
23+
[ OK ] /CN=github.com
24+
```
25+
26+
For further information about the command line tool `tls-ping` see the following
27+
help output.
28+
29+
```bash
30+
Usage:
31+
tls-ping [OPTIONS] HOST PORT
32+
33+
Parameters:
34+
HOST hostname to ping
35+
PORT port to ping
36+
37+
Options:
38+
-s, --starttls use STARTTLS
39+
-t, --timeout SECONDS timeout in seconds (default: 5)
40+
-q, --quiet suppress output
41+
-h, --help print help
42+
-m, --man show manpage
43+
-v, --version show version
44+
```
45+
46+
## License
47+
48+
[MIT License](https://spdx.org/licenses/MIT.html)
49+
50+
## Is it any good?
51+
52+
[Yes.](https://news.ycombinator.com/item?id=3067434)

Rakefile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
require 'bundler/gem_tasks'
4+
require 'rspec/core/rake_task'
5+
require 'rubocop/rake_task'
6+
7+
FileList['tasks/**/*.rake'].each { |f| import(f) }
8+
9+
RSpec::Core::RakeTask.new(:rspec)
10+
RuboCop::RakeTask.new
11+
12+
desc "Run tasks 'rubocop' by default."
13+
task default: %w[rubocop]

bin/tls-ping

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require 'tls/ping/app'
5+
6+
TLS::Ping::App::Command.run

lib/tls.rb

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'tls/ping'
4+
5+
# :nodoc:
6+
module TLS
7+
class << self
8+
def ping(...)
9+
TLS::Ping.new(...).succeeded!
10+
end
11+
end
12+
end

lib/tls/ping.rb

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# frozen_string_literal: true
2+
3+
require 'openssl'
4+
require 'socket'
5+
require 'timeout'
6+
7+
module TLS
8+
class Ping
9+
VERSION = '0.1.0'
10+
11+
attr_reader :error, :peer_cert
12+
13+
def initialize(host, port, starttls: false, timeout: 5)
14+
@host = host
15+
@port = port
16+
@starttls = starttls
17+
@timeout = timeout
18+
19+
execute
20+
end
21+
22+
def succeeded?
23+
@error.nil?
24+
end
25+
26+
def succeeded!
27+
raise @error if @error
28+
end
29+
30+
private
31+
32+
def execute
33+
socket = Timeout.timeout(@timeout) do
34+
socket = TCPSocket.new(@host, @port)
35+
socket.timeout = @timeout
36+
socket
37+
end
38+
39+
starttls(socket) if @starttls
40+
41+
tls_socket = OpenSSL::SSL::SSLSocket.new(socket, tls_ctx)
42+
tls_socket.hostname = @host
43+
tls_socket.connect
44+
rescue StandardError => e
45+
@error = e
46+
ensure
47+
@peer_cert = tls_socket&.peer_cert || tls_socket&.peer_cert_chain&.first
48+
tls_socket&.close
49+
socket&.close
50+
end
51+
52+
def tls_ctx
53+
OpenSSL::SSL::SSLContext.new.tap do |ctx|
54+
store = OpenSSL::X509::Store.new
55+
store.set_default_paths
56+
57+
ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
58+
ctx.cert_store = store
59+
ctx.timeout = @timeout
60+
end
61+
end
62+
63+
def starttls(socket)
64+
return if !@starttls
65+
66+
socket.gets
67+
socket.write("EHLO tls.ping\r\n")
68+
69+
loop do
70+
break if socket.gets.start_with?('250 ')
71+
end
72+
73+
socket.write("STARTTLS\r\n")
74+
socket.gets
75+
end
76+
end
77+
end

lib/tls/ping/app.rb

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'app/base'
4+
require_relative '../../tls'
5+
6+
module TLS
7+
class Ping
8+
module App
9+
class Command < TLS::Ping::App::BaseCommand
10+
parameter 'HOST', 'hostname to ping'
11+
parameter 'PORT', 'port to ping'
12+
option ['-s', '--starttls'], :flag, 'use STARTTLS'
13+
option ['-t', '--timeout'], 'SECONDS', 'timeout in seconds', default: 5
14+
option ['-q', '--quiet'], :flag, 'suppress output'
15+
16+
PING_OK = 0
17+
PING_FAIL = 1
18+
PING_UNKNOWN = 255
19+
20+
def execute
21+
header
22+
code, reason = action
23+
result(code, reason:)
24+
25+
exit(code)
26+
end
27+
28+
private
29+
30+
def header
31+
return if quiet?
32+
33+
puts "> #{host}:#{port}" if !quiet?
34+
end
35+
36+
def action
37+
ping = TLS::Ping.new(
38+
host,
39+
port,
40+
starttls: starttls?,
41+
timeout: timeout.to_f
42+
)
43+
ping.succeeded!
44+
45+
reason = ping.peer_cert.subject.to_s
46+
[PING_OK, reason]
47+
rescue OpenSSL::SSL::SSLError => e
48+
reason = e.message.split(': ').last.capitalize
49+
[PING_FAIL, reason]
50+
rescue StandardError
51+
[PING_UNKNOWN]
52+
end
53+
54+
def result(code, reason: nil)
55+
return if quiet?
56+
57+
status = {
58+
PING_OK => Pastel.new.green.bold('OK'),
59+
PING_FAIL => Pastel.new.red.bold('FAIL'),
60+
PING_UNKNOWN => Pastel.new.yellow.bold('UNKNOWN')
61+
}[code]
62+
63+
info = " [ #{status} ]"
64+
info += " #{reason}" if reason
65+
66+
puts info
67+
end
68+
end
69+
end
70+
end
71+
end

0 commit comments

Comments
 (0)