1
0
مراية لـ https://github.com/postalserver/postal.git تم المزامنة 2026-01-17 13:39:46 +00:00

refactor: remove the fast server

هذا الالتزام موجود في:
Adam Cooke
2021-07-29 10:54:15 +00:00
الأصل 8e3294ba1a
التزام 17dd7cc757
38 ملفات معدلة مع 116 إضافات و861 حذوفات

عرض الملف

@@ -9,7 +9,6 @@ module Postal
autoload :Countries
autoload :DKIMHeader
autoload :Error
autoload :FastServer
autoload :Helpers
autoload :HTTP
autoload :HTTPSender
@@ -29,6 +28,7 @@ module Postal
autoload :SMTPSender
autoload :SMTPServer
autoload :SpamCheck
autoload :TrackingMiddleware
autoload :UserCreator
autoload :Version
autoload :Worker
@@ -37,7 +37,6 @@ module Postal
def self.eager_load!
super
Postal::MessageDB.eager_load!
Postal::FastServer.eager_load!
Postal::SMTPServer.eager_load!
end

عرض الملف

@@ -151,35 +151,6 @@ module Postal
end
end
def self.fast_server_default_private_key_path
config.fast_server.default_private_key_path || config_root.join('fast_server.key')
end
def self.fast_server_default_private_key
@fast_server_default_private_key ||= OpenSSL::PKey::RSA.new(File.read(fast_server_default_private_key_path))
end
def self.fast_server_default_certificate_path
config.fast_server.default_tls_certificate_path || config_root.join('fast_server.cert')
end
def self.fast_server_default_certificate_data
@fast_server_default_certificate_data ||= File.read(fast_server_default_certificate_path)
end
def self.fast_server_default_certificates
@fast_server_default_certificates ||= begin
certs = self.fast_server_default_certificate_data.scan(/-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m)
certs.map do |c|
OpenSSL::X509::Certificate.new(c)
end.freeze
end
end
def self.lets_encrypt_private_key_path
@lets_encrypt_private_key_path ||= Postal.config_root.join('lets_encrypt.pem')
end
def self.signing_key_path
config_root.join('signing.key')
end
@@ -203,19 +174,11 @@ module Postal
raise ConfigError, "No config found at #{self.config_file_path}"
end
unless File.exists?(self.lets_encrypt_private_key_path)
raise ConfigError, "No Let's Encrypt private key found at #{self.lets_encrypt_private_key_path}"
end
unless File.exists?(self.signing_key_path)
raise ConfigError, "No signing key found at #{self.signing_key_path}"
end
end
def self.tracking_available?
self.config.fast_server.enabled?
end
def self.ip_pools?
self.config.general.use_ip_pools?
end

عرض الملف

@@ -1,12 +0,0 @@
module Postal
module FastServer
extend ActiveSupport::Autoload
eager_autoload do
autoload :Client
autoload :HTTPHeader
autoload :HTTPHeaderSet
autoload :Interface
autoload :Server
end
end
end

عرض الملف

@@ -1,172 +0,0 @@
require 'stringio'
module Postal
module FastServer
class Client
class ClientWentAway < StandardError; end
class BadRequest < StandardError; end
def initialize(socket, options)
@raw_socket = socket
@options = options
end
def run
Timeout.timeout(15) do
if Postal.config.fast_server.proxy_protocol
# gets without readahead
line = ""
char = nil
while(char != "\n")
char = @raw_socket.read(1)
line << char
end
line.chomp!
if m = line.match(/\APROXY (.+) (.+) (.+) (.+) (.+)\z/)
@remote_ip = m[2]
else
return false
end
end
if self.ssl?
@socket = OpenSSL::SSL::SSLSocket.new(@raw_socket, self.class.ssl_context)
@socket.accept
else
@socket = @raw_socket
end
Timeout::timeout(20) do
# Read the request line
request = @socket.gets.to_s.chomp
# Split the request into its 3 parts
method, path, protocol = request.split(' ', 3)
raise BadRequest unless method && path && protocol
# Create an empty header set
header_set = HTTPHeaderSet.new
# Read each header and populate the header set
loop do
header = @socket.gets
if header.nil?
raise ClientWentAway
elsif header.chomp == ""
break
else
header_set << HTTPHeader.from_string(header.chomp)
end
end
# At this point, one might want to read the request body, but I don't think we need it.
# Build rack request
server_name, server_port = header_set['Host'].try(:value).to_s.split(":", 2)
request = {
"REQUEST_METHOD" => method,
"SCRIPT_NAME" => "",
"PATH_INFO" => path.split('?', 2)[0],
"QUERY_STRING" => path.split('?', 2)[1],
"SERVER_NAME" => server_name || "",
"SERVER_PORT" => server_name || "",
"rack.version" => [1, 3],
"rack.url_scheme" => ssl? ? "https" : "http",
"rack.input" => StringIO.new(""),
"rack.errors" => STDERR,
"rack.multithread" => true,
"rack.multiprocess" => true,
"rack.run_once" => false,
"rack.hijack" => false,
"rack.hijack_io" => false,
"REMOTE_ADDR" => remote_ip,
}
# Add request headers to rack hash
header_set.headers.each do |header|
request["HTTP_" + header.key.gsub('-', '_').upcase] = header.value
end
# Call the rack app and process the result
code, headers, body = Interface.new.call(request)
response = "HTTP/1.1 #{code} #{Rack::Utils::HTTP_STATUS_CODES[code]}\r\n"
headers.each do |k,v|
response << "#{k}:#{v}\r\n"
end
response << "\r\n"
body.each do |data|
response << data
end
@socket.write(response)
end
end
rescue ClientWentAway, Timeout::Error, Errno::ECONNRESET
# We don't really care if a client has disapeared, close the sockets and carry on.
rescue OpenSSL::SSL::SSLError
# Don't worry about SSL negotiation failures, disconnect and carry on
rescue BadRequest
# We couldn't read a proper HTTP request, disconnect the client
rescue => e
if defined?(Raven)
Raven.capture_exception(e)
end
ensure
@socket.close rescue nil
@raw_socket.close rescue nil
end
def ssl?
!!@options[:ssl]
end
def remote_ip
@remote_ip || @raw_socket.peeraddr[3].sub('::ffff:', '')
end
def self.ssl_context(domain_name = nil)
@ssl_certificates ||= {}
unless @ssl_certificates_refreshed && @ssl_certificates_refreshed > Time.now.utc.beginning_of_day
@ssl_certificates_refreshed = Time.now.utc
@ssl_certificates = {}
end
@ssl_certificates[domain_name] ||= OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
if domain_name
if domain = TrackCertificate.active.where(:domain => domain_name).first
ssl_context.cert = domain.certificate_object
ssl_context.extra_chain_cert = domain.intermediaries_array
ssl_context.key = domain.key_object
end
end
if ssl_context.cert.nil?
ssl_context.cert = Postal.fast_server_default_certificates[0]
ssl_context.extra_chain_cert = Postal.fast_server_default_certificates[1..-1]
ssl_context.key = Postal.fast_server_default_private_key
end
ssl_context.ssl_version = "SSLv23"
ssl_context.ciphers = 'EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4 !DH'
ssl_context.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options] |
OpenSSL::SSL::OP_NO_SSLv2 |
OpenSSL::SSL::OP_NO_SSLv3 |
OpenSSL::SSL::OP_NO_COMPRESSION |
OpenSSL::SSL::OP_CIPHER_SERVER_PREFERENCE
if ssl_context.respond_to?('tmp_ecdh_callback=')
ssl_context.tmp_ecdh_callback = Proc.new do |*a|
OpenSSL::PKey::EC.new("prime256v1")
end
end
unless domain_name
ssl_context.servername_cb = Proc.new do |ctx, hostname|
self.ssl_context(hostname)
end
end
end
end
end
end
end

عرض الملف

@@ -1,21 +0,0 @@
module Postal
module FastServer
class HTTPHeader
attr_accessor :key, :value
def self.from_string(string)
k, v = string.to_s.split(/\:\s*/, 2)
self.new(k.to_s, v.to_s)
end
def initialize(k, v)
@key = k
@value = v
end
def to_s
@key + ": " + @value
end
end
end
end

عرض الملف

@@ -1,37 +0,0 @@
module Postal
module FastServer
class HTTPHeaderSet
attr_accessor :headers
def initialize
@headers = []
end
def self.from_string_array(array)
header_set = self.new
header_set.headers = array.map{|h|HTTPHeader.from_string(h)}
header_set
end
def select(key)
@headers.select{|h|h.key.downcase == key.downcase}
end
def [](key)
@headers.find{|h|h.key.downcase == key.downcase}
end
def []=(key, value)
self.delete(key)
@headers << HTTPHeader.new(key, value)
end
def delete(key)
@headers.delete_if{|h|h.key.downcase == key.downcase}
end
def <<(header)
@headers << header
end
end
end
end

عرض الملف

@@ -1,96 +0,0 @@
module Postal
module FastServer
class Interface
# TODO: Make this multithreaded? Thread-safe?
TRACKING_PIXEL = File.read(Rails.root.join('app', 'assets', 'images', 'tracking_pixel.png'))
def get_message_db_from_server_token(token)
if server = ::Server.find_by_token(token)
server.message_db
else
nil
end
end
def call(env)
request = Rack::Request.new(env)
if request.path =~ /\A\/(\.well-known\/.*)/
if certificate = ::TrackCertificate.find_by_verification_path($1)
return [200, {'Content-Length' => certificate.verification_string.bytesize.to_s}, [certificate.verification_string]]
else
return [404, {}, ["Verification not found"]]
end
elsif request.path =~ /\A\/img\/([a-z0-9\-]+)\/([a-z0-9\-]+)/i
server_token = $1
message_token = $2
if message_db = get_message_db_from_server_token(server_token)
begin
message = message_db.message(:token => message_token)
message.create_load(request)
rescue Postal::MessageDB::Message::NotFound
# This message has been removed, we'll just continue to serve the image
rescue => e
# Somethign else went wrong. We don't want to stop the image loading though because
# this is our problem. Log this exception though.
if defined?(Raven)
Raven.capture_exception(e)
end
end
source_image = request.params['src']
if source_image.nil?
headers = {}
headers['Content-Type'] = "image/png"
headers['Content-Length'] = TRACKING_PIXEL.bytesize.to_s
return [200, headers, [TRACKING_PIXEL]]
elsif source_image =~ /\Ahttps?\:\/\//
response = Postal::HTTP.get(source_image, :timeout => 3)
if response[:code] == 200
headers = {}
headers['Content-Type'] = response[:headers]['content-type']&.first
headers['Last-Modified'] = response[:headers]['last-modified']&.first
headers['Cache-Control'] = response[:headers]['cache-control']&.first
headers['Etag'] = response[:headers]['etag']&.first
headers['Content-Length'] = response[:body].bytesize.to_s
return [200, headers, [response[:body]]]
else
return [404, {}, ['Not found']]
end
else
return [400, {}, ['Invalid/missing source image']]
end
else
return [404, {}, ['Invalid Server Token']]
end
end
if request.path =~ /\A\/([a-z0-9\-]+)\/([a-z0-9\-]+)/i
server_token = $1
link_token = $2
if message_db = get_message_db_from_server_token(server_token)
if link = message_db.select(:links, :where => {:token => link_token}, :limit => 1).first
time = Time.now.to_f
if link['message_id']
message_db.update(:messages, {:clicked => time}, :where => {:id => link['message_id']})
message_db.insert(:clicks, {:message_id => link['message_id'], :link_id => link['id'], :ip_address => request.ip, :user_agent => request.user_agent, :timestamp => time})
SendWebhookJob.queue(:main, :server_id => message_db.server_id, :event => 'MessageLinkClicked', :payload => {:_message => link['message_id'], :url => link['url'], :token => link['token'], :ip_address => request.ip, :user_agent => request.user_agent})
end
return [307, {'Location' => link['url']}, ["Redirected to: #{link['url']}"]]
else
return [404, {}, ['Link not found']]
end
else
return [404, {}, ['Invalid Server Token']]
end
end
[200, {}, ["Hello."]]
end
end
end
end

عرض الملف

@@ -1,49 +0,0 @@
require 'socket'
require 'openssl'
module Postal
module FastServer
class Server
def run
if Postal.config.fast_server.bind_address.blank?
Postal.logger_for(:fast_server).info "Cannot start fast server because no bind address has been specified"
exit 1
end
Thread.abort_on_exception = true
TrackCertificate
bind_addresses = Postal.config.fast_server.bind_address
bind_addresses = [bind_addresses] unless bind_addresses.is_a?(Array)
server_sockets = bind_addresses.each_with_object({}) do |bind_addr, sockets|
sockets[TCPServer.new(bind_addr, Postal.config.fast_server.port)] = {:ssl => false}
sockets[TCPServer.new(bind_addr, Postal.config.fast_server.ssl_port)] = {:ssl => true}
Postal.logger_for(:fast_server).info("Fast server started listening on HTTP (#{bind_addr}:#{Postal.config.fast_server.port})")
Postal.logger_for(:fast_server).info("Fast server started listening on HTTPS port (#{bind_addr}:#{Postal.config.fast_server.ssl_port})")
end
loop do
client = nil
ios = select(server_sockets.keys, nil, nil, 1)
if ios && server_io = ios[0][0]
begin
client_io = server_io.accept_nonblock
client = Client.new(client_io, server_sockets[server_io])
Thread.new(client) { |t_client| t_client.run }
rescue IO::WaitReadable, Errno::EINTR
# Never mind, guess the client went away
rescue => e
if defined?(Raven)
Raven.capture_exception(e)
end
client_io.close rescue nil
end
end
end
end
end
end
end

عرض الملف

@@ -1,29 +0,0 @@
require 'acme-client'
module Postal
module LetsEncrypt
def self.client
@client ||= Acme::Client.new(:private_key => private_key, :directory => directory)
end
def self.private_key
@private_key ||= OpenSSL::PKey::RSA.new(File.open(Postal.lets_encrypt_private_key_path))
end
def self.directory
@directory ||= Rails.env.development? ? "https://acme-staging-v02.api.letsencrypt.org/directory" : "https://acme-v02.api.letsencrypt.org/directory"
end
def self.register_private_key(email_address)
registration = client.new_account(:contact => "mailto:#{email_address}", :terms_of_service_agreed => true)
logger.info "Successfully registered private key with address #{email_address}"
true
end
def self.logger
Postal.logger_for(:lets_encrypt)
end
end
end

عرض الملف

@@ -80,11 +80,11 @@ module Postal
end
def parse(part, type = nil)
if Postal.tracking_available? && @domain.track_clicks?
if @domain.track_clicks?
part = insert_links(part, type)
end
if Postal.tracking_available? && @domain.track_loads? && type == :html
if @domain.track_loads? && type == :html
part = insert_tracking_image(part)
end

عرض الملف

@@ -0,0 +1,105 @@
module Postal
class TrackingMiddleware
TRACKING_PIXEL = File.read(Rails.root.join('app', 'assets', 'images', 'tracking_pixel.png'))
def initialize(app = nil)
@app = app
end
def call(env)
unless env['HTTP_X_POSTAL_TRACK_HOST'].to_i == 1
return @app.call(env)
end
request = Rack::Request.new(env)
case request.path
when /\A\/img\/([a-z0-9\-]+)\/([a-z0-9\-]+)/i
server_token = $1
message_token = $2
dispatch_image_request(request, server_token, message_token)
when /\A\/([a-z0-9\-]+)\/([a-z0-9\-]+)/i
server_token = $1
link_token = $2
dispatch_redirect_request(request, server_token, link_token)
else
[200, {}, ["Hello."]]
end
end
private
def dispatch_image_request(request, server_token, message_token)
message_db = get_message_db_from_server_token(server_token)
if message_db.nil?
return [404, {}, ['Invalid Server Token']]
end
begin
message = message_db.message(:token => message_token)
message.create_load(request)
rescue Postal::MessageDB::Message::NotFound
# This message has been removed, we'll just continue to serve the image
rescue => e
# Somethign else went wrong. We don't want to stop the image loading though because
# this is our problem. Log this exception though.
Raven.capture_exception(e) if defined?(Raven)
end
source_image = request.params['src']
case source_image
when nil
headers = {}
headers['Content-Type'] = "image/png"
headers['Content-Length'] = TRACKING_PIXEL.bytesize.to_s
return [200, headers, [TRACKING_PIXEL]]
when /\Ahttps?\:\/\//
response = Postal::HTTP.get(source_image, :timeout => 3)
if response[:code] == 200
headers = {}
headers['Content-Type'] = response[:headers]['content-type']&.first
headers['Last-Modified'] = response[:headers]['last-modified']&.first
headers['Cache-Control'] = response[:headers]['cache-control']&.first
headers['Etag'] = response[:headers]['etag']&.first
headers['Content-Length'] = response[:body].bytesize.to_s
return [200, headers, [response[:body]]]
else
return [404, {}, ['Not found']]
end
else
return [400, {}, ['Invalid/missing source image']]
end
end
def dispatch_redirect_request(request, server_token, link_token)
message_db = get_message_db_from_server_token(server_token)
if message_db.nil?
return [404, {}, ['Invalid Server Token']]
end
link = message_db.select(:links, :where => {:token => link_token}, :limit => 1).first
if link.nil?
return [404, {}, ['Link not found']]
end
time = Time.now.to_f
if link['message_id']
message_db.update(:messages, {:clicked => time}, :where => {:id => link['message_id']})
message_db.insert(:clicks, {:message_id => link['message_id'], :link_id => link['id'], :ip_address => request.ip, :user_agent => request.user_agent, :timestamp => time})
SendWebhookJob.queue(:main, :server_id => message_db.server_id, :event => 'MessageLinkClicked', :payload => {:_message => link['message_id'], :url => link['url'], :token => link['token'], :ip_address => request.ip, :user_agent => request.user_agent})
end
return [307, {'Location' => link['url']}, ["Redirected to: #{link['url']}"]]
end
def get_message_db_from_server_token(token)
if server = ::Server.find_by_token(token)
server.message_db
else
nil
end
end
end
end

عرض الملف

@@ -28,11 +28,6 @@ namespace :postal do
end
end
desc 'Start the fast server'#
task :fast_server => :environment do
Postal::FastServer::Server.new.run
end
end
Rake::Task['db:migrate'].enhance do