Skip to content

Commit

Permalink
TLS ping host and port. 🔑
Browse files Browse the repository at this point in the history
  • Loading branch information
tschaefer committed Apr 28, 2024
0 parents commit 9b80d9d
Show file tree
Hide file tree
Showing 16 changed files with 688 additions and 0 deletions.
61 changes: 61 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Created by https://www.toptal.com/developers/gitignore/api/ruby
# Edit at https://www.toptal.com/developers/gitignore?templates=ruby

### Ruby ###
*.gem
*.rbc
/.config
/coverage/
/InstalledFiles
/pkg/
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/tmp/

# Used by dotenv library to load environment variables.
# .env

# Ignore Byebug command history file.
.byebug_history

## Specific to RubyMotion:
.dat*
.repl_history
build/
*.bridgesupport
build-iPhoneOS/
build-iPhoneSimulator/

## Specific to RubyMotion (use of CocoaPods):
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# vendor/Pods/

## Documentation cache and generated files:
/.yardoc/
/_yardoc/
/doc/
/rdoc/

## Environment normalization:
/.bundle/
/vendor/bundle
/lib/bundler/man/

# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
Gemfile.lock
.ruby-version
.ruby-gemset

# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc

# Used by RuboCop. Remote config files pulled in from inherit_from directive.
# .rubocop-https?--*

# End of https://www.toptal.com/developers/gitignore/api/ruby
1 change: 1 addition & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
24 changes: 24 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
require:
- rubocop-rspec
- rubocop-rake
- rubocop-performance

AllCops:
NewCops: enable

Style/NegatedIf:
Enabled: false

Metrics/MethodLength:
CountAsOne:
- 'array'
- 'hash'
- 'heredoc'
Max: 25

Metrics/AbcSize:
CountRepeatedAttributes: false

Style/Documentation:
Enabled: false
22 changes: 22 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

source 'https://rubygems.org'
gemspec

group :development, :test do
gem 'rake'

gem 'pry'
gem 'pry-byebug'
gem 'pry-doc'
gem 'pry-rescue'

gem 'rspec'

gem 'rubocop'
gem 'rubocop-performance'
gem 'rubocop-rake'
gem 'rubocop-rspec'

gem 'simplecov'
end
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Tobias Schäfer

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# tls-ping

TLS ping host and port.

## Introduction

**tls-ping** connects to a given host and port and validates the TLS connection
and certificate.

## Installation

```bash
gem build
version=$(ruby -Ilib -e 'require "tls/ping"; puts TLS::Ping::VERSION')
gem install tls-ping-${version}.gem
```

## Usage

```bash
$ tls-ping github.com 443
> github.com:443
[ OK ] /CN=github.com
```

For further information about the command line tool `tls-ping` see the following
help output.

```bash
Usage:
tls-ping [OPTIONS] HOST PORT

Parameters:
HOST hostname to ping
PORT port to ping

Options:
-s, --starttls use STARTTLS
-t, --timeout SECONDS timeout in seconds (default: 5)
-q, --quiet suppress output
-h, --help print help
-m, --man show manpage
-v, --version show version
```

## License

[MIT License](https://spdx.org/licenses/MIT.html)

## Is it any good?

[Yes.](https://news.ycombinator.com/item?id=3067434)
13 changes: 13 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'

FileList['tasks/**/*.rake'].each { |f| import(f) }

RSpec::Core::RakeTask.new(:rspec)
RuboCop::RakeTask.new

desc "Run tasks 'rubocop' by default."
task default: %w[rubocop]
6 changes: 6 additions & 0 deletions bin/tls-ping
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require 'tls/ping/app'

TLS::Ping::App::Command.run
12 changes: 12 additions & 0 deletions lib/tls.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

require_relative 'tls/ping'

# :nodoc:
module TLS
class << self
def ping(...)
TLS::Ping.new(...).succeeded!
end
end
end
77 changes: 77 additions & 0 deletions lib/tls/ping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

require 'openssl'
require 'socket'
require 'timeout'

module TLS
class Ping
VERSION = '0.1.0'

attr_reader :error, :peer_cert

def initialize(host, port, starttls: false, timeout: 5)
@host = host
@port = port
@starttls = starttls
@timeout = timeout

execute
end

def succeeded?
@error.nil?
end

def succeeded!
raise @error if @error
end

private

def execute
socket = Timeout.timeout(@timeout) do
socket = TCPSocket.new(@host, @port)
socket.timeout = @timeout
socket
end

starttls(socket) if @starttls

tls_socket = OpenSSL::SSL::SSLSocket.new(socket, tls_ctx)
tls_socket.hostname = @host
tls_socket.connect
rescue StandardError => e
@error = e
ensure
@peer_cert = tls_socket&.peer_cert || tls_socket&.peer_cert_chain&.first
tls_socket&.close
socket&.close
end

def tls_ctx
OpenSSL::SSL::SSLContext.new.tap do |ctx|
store = OpenSSL::X509::Store.new
store.set_default_paths

ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
ctx.cert_store = store
ctx.timeout = @timeout
end
end

def starttls(socket)
return if !@starttls

socket.gets
socket.write("EHLO tls.ping\r\n")

loop do
break if socket.gets.start_with?('250 ')
end

socket.write("STARTTLS\r\n")
socket.gets
end
end
end
71 changes: 71 additions & 0 deletions lib/tls/ping/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# frozen_string_literal: true

require_relative 'app/base'
require_relative '../../tls'

module TLS
class Ping
module App
class Command < TLS::Ping::App::BaseCommand
parameter 'HOST', 'hostname to ping'
parameter 'PORT', 'port to ping'
option ['-s', '--starttls'], :flag, 'use STARTTLS'
option ['-t', '--timeout'], 'SECONDS', 'timeout in seconds', default: 5
option ['-q', '--quiet'], :flag, 'suppress output'

PING_OK = 0
PING_FAIL = 1
PING_UNKNOWN = 255

def execute
header
code, reason = action
result(code, reason:)

exit(code)
end

private

def header
return if quiet?

puts "> #{host}:#{port}" if !quiet?
end

def action
ping = TLS::Ping.new(
host,
port,
starttls: starttls?,
timeout: timeout.to_f
)
ping.succeeded!

reason = ping.peer_cert.subject.to_s
[PING_OK, reason]
rescue OpenSSL::SSL::SSLError => e
reason = e.message.split(': ').last.capitalize
[PING_FAIL, reason]
rescue StandardError
[PING_UNKNOWN]
end

def result(code, reason: nil)
return if quiet?

status = {
PING_OK => Pastel.new.green.bold('OK'),
PING_FAIL => Pastel.new.red.bold('FAIL'),
PING_UNKNOWN => Pastel.new.yellow.bold('UNKNOWN')
}[code]

info = " [ #{status} ]"
info += " #{reason}" if reason

puts info
end
end
end
end
end
Loading

0 comments on commit 9b80d9d

Please sign in to comment.