objects/endpoint.rb
# frozen_string_literal: true
# Copyright (c) 2017-2024 Yegor Bugayenko
#
# 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.
require 'uri'
require 'timeout'
require 'net/http'
require 'openssl'
require_relative 'resource'
require_relative 'proxied_resource'
require_relative 'counted_resource'
require_relative 'total_pings'
require_relative 'endpoint/ep_uri'
require_relative 'endpoint/ep_state'
require_relative 'endpoint/ep_availability'
require_relative 'endpoint/ep_badge'
require_relative 'endpoint/ep_graph'
#
# Single endpoint
#
class Endpoint
# Cached endpoint
class Cached
def initialize(point)
@point = point
@history = nil
end
def to_h
@point.to_h
end
def history
@history ||= @point.history
end
def ping
@point.ping
end
def flush
@point.flush
end
end
def initialize(aws, item)
@aws = aws
@item = item
end
def to_h
h = {
uri: URI.parse(@item['uri']),
favicon: @item['favicon'] ? URI.parse(@item['favicon']) : nil,
login: @item['login'],
id: @item['id'],
hostname: @item['hostname'],
failures: @item['failures'].to_i,
pings: @item['pings'].to_i,
up: @item['state'] == 'up',
created: Time.at(@item['created']),
log: @item['log']
}
h[:updated] = Time.at(@item['updated']) if @item['updated']
h[:flipped] = Time.at(@item['flipped']) if @item['flipped']
h[:expires] = Time.at(@item['expires']) if @item['expires']
h
end
def history
@aws.query(
table_name: 'sn-pings',
select: 'SPECIFIC_ATTRIBUTES',
projection_expression: '#time, msec, code',
limit: 1000,
scan_index_forward: false,
expression_attribute_names: {
'#time' => 'time'
},
expression_attribute_values: {
':u' => @item['uri'],
':t' => (Time.now - (1000 * 60)).to_i
},
key_condition_expression: 'uri = :u and #time > :t'
).items.map do |i|
{
time: Time.at(i['time']),
msec: i['msec'].to_i,
code: i['code'].to_i
}
end
end
def ping(count, proxies = [''])
start = Time.now
h = to_h
code, body, log = ProxiedResource.new(
CountedResource.new(
count,
Resource.new(h[:uri])
),
proxies
).take
@aws.put_item(
table_name: 'sn-pings',
item: {
uri: h[:uri].to_s,
code: code,
time: Time.now.to_i,
msec: ((Time.now - start) * 1000).to_i,
local: 'unknown',
remote: 'unknown',
delete_on: (Time.now + (24 * 60 * 60)).to_i
}
)
up = code >= 200 && code < 300
update = [
'updated = :t',
'expires = :e',
'favicon = :f',
'pings = pings + :o',
'#state = :s'
]
ean = {
'#state' => 'state'
}
eav = {
':s' => up ? 'up' : 'down',
':o' => 1,
':t' => Time.now.to_i,
':e' => (Time.now + 60).to_i, # ping again in 60 seconds
':f' => favicon(body).to_s
}
update << 'failures = failures + :o' unless up
update << 'flipped = :t' unless up == h[:up]
unless up
update << '#log = :g'
ean['#log'] = 'log'
eav[':g'] = "#{Time.now}\n\n#{log}"
end
@aws.update_item(
table_name: 'sn-endpoints',
key: {
'login' => h[:login],
'uri' => h[:uri].to_s
},
expression_attribute_names: ean,
expression_attribute_values: eav,
update_expression: "set #{update.join(', ')}"
)
yield(up, self) if block_given? && up != h[:up]
"#{h[:uri]}: #{code}"
end
def flush
h = to_h
@aws.update_item(
table_name: 'sn-endpoints',
key: {
'login' => h[:login],
'uri' => h[:uri].to_s
},
expression_attribute_names: {
'#log' => 'log',
'#failures' => 'failures'
},
expression_attribute_values: {
':f' => 0
},
update_expression: 'REMOVE #log SET #failures=:f'
)
end
private
def favicon(body)
xml = Nokogiri::HTML(body)
links = xml.xpath('/html/head/link[@rel="shortcut icon"]/@href')
uri = to_h[:uri]
raise "Favicon URI can't be nil in the endpoint" if uri.nil?
if links.empty?
URI.parse("http://#{uri.host}/favicon.ico")
else
uri = URI.parse(links[0])
uri = URI.parse("http://#{uri.host}#{uri}") unless uri.absolute?
uri
end
rescue StandardError => _e
URI.parse('localhost')
end
end