Artifact [f7c82e714f]

Artifact f7c82e714f120b3c99f2da1fc85708a956cc68f8:


#! /usr/bin/env crystal

require "openssl"
require "openssl/digest"
require "http/client"
require "file.cr"

/*
 * Crystal 0.10 has a bug in the OpenSSL::Digest.update method, rewrite it here.
 */
module OpenSSL
  module DigestBase
    def update(io : IO)
      buffer :: UInt8[2048]
      while (read_bytes = io.read(buffer.to_slice)) > 0
        self << buffer.to_slice[0, read_bytes]
      end
      self
    end
  end
end

private def getHashInformation(hashMethod : String) : Hash
	begin
		hashData = OpenSSL::Digest.new(hashMethod)
	rescue
		raise "Unacceptable hashing algorithm: #{hashMethod}"
		
	end

	returnValue = {
		length: hashData.digest_size
	}

	return returnValue
end

private def validateFile(fileName, hashMethod, hashValue) : Bool
	checkHash = OpenSSL::Digest.new(hashMethod)

	checkHash.file(fileName)

	if checkHash.hexdigest === hashValue
		return true
	end

	return false
end

private def fetchFile(url : String, cacheFileName, hashMethod, hashValue)
	if (url =~ /^https?:\/\//).nil?
		raise "Invalid URL scheme\nWe only support HTTP, and HTTPS transports for origin URLs.  The URL must begin with one of: http://, https://"
	end

	tempFileName = "#{cacheFileName}-#{Random.rand}-#{Random.rand}-#{Random.rand}"

	Dir.mkdir_p(File.dirname(cacheFileName), mode = 0o755);

	HTTP::Client.get(url) { |result|
		if result.status_code != 200
			raise "Unable to fetch remote file, HTTP status code: #{result.status_code}"
		end

		if ! result.body_io?
			raise "Unable to fetch remote file, no body"
		end

		begin
			tempFile = File.open(tempFileName, "w");

			IO.copy(result.body_io, tempFile.to_fd_io);
		ensure
			if tempFile
				tempFile.close
			end
		end
	}

	if ! validateFile(tempFileName, hashMethod, hashValue)
		if File.exists?(tempFileName)
			File.delete(tempFileName)
		end

		raise "Unable to fetch remote file, validation failed"
	end

	File.rename(tempFileName, cacheFileName)
end

private def deliverFile(fileName)
	file = File.open(fileName)

	IO.copy(file, STDOUT)

	file.close
end

private def handleRequest(hashMethod, hashValue, upstreamURL = Nil)
	hashInfo = getHashInformation(hashMethod)

	if (hashValue.size != (hashInfo[:length] * 2))
		raise "Hash length is incorrect, it should be #{hashInfo[:length] * 2} hex digits long"
	end

	if (hashValue =~ /^[0-9a-f]*$/).nil?
		raise "Hash value is incorrect, it should contain only hex digits"
	end

	cacheFileName = hashMethod + "/" + hashValue

	if File.exists?(cacheFileName)
		return cacheFileName
	end

	if upstreamURL.is_a?(Nil)
		raise "Contents are not locally cached and no upstream URL has been provided"
	end

	fetchFile(upstreamURL, cacheFileName, hashMethod, hashValue)

	return cacheFileName
end

private def parseURL(url : String)
	urlParts = url.split("/")

	case urlParts.size
		when 1
			raise "No hash method or hash value specified."
		when 2
			raise "No hash value specified"
		when 3
			hashMethod = urlParts[1];
			hashValue = urlParts[2];
		else
			raise "Too many components specified in pathname"
	end

	return [hashMethod, hashValue]
end

private def createURL : String
	host = Nil

	["HTTP_HOST", "SERVER_NAME"].each { |hostEntry|
		if ENV[hostEntry]?
			host = ENV[hostEntry]

			break
		end
	}

	if host.is_a?(Nil)
		return "<unknown>"
	end

	if ENV["HTTPS"]? && ENV["HTTPS"] === "on"
		scheme = "https"
	else
		scheme = "http"
	end

	addPort = ""

	if ENV["SERVER_PORT"]?
		defaultPort = if scheme == "https"
			443
		else
			80
		end

		if  ENV["SERVER_PORT"].to_i != defaultPort
			addPort = ":#{ENV["SERVER_PORT"]}"
		end
	end

	return "#{scheme}://#{host}#{addPort}/"
end

private def cgiInterface
	if ENV["HTTP_X_CACHE_URL"]?
		upstreamURL = ENV["HTTP_X_CACHE_URL"]
	end

	begin
		hashMethod, hashValue = parseURL(ENV["REQUEST_URI"])

		outputFile = handleRequest(hashMethod, hashValue, upstreamURL)
	rescue reason
		puts "Status: 400 Not OK"
		puts "Content-type: text/plain"
		puts ""
		puts "Usage: #{createURL()}<hashMethod>/<hashValue>"
		puts "       Supply X-Cache-URL header with the origin URL to cache, it will be fetched and cached if the contents are not already available"
		puts ""
		puts "Example: curl --fail --header 'X-Cache-URL: http://www.rkeene.org/' http://hashcache.rkeene.org/sha1/dfc00e1a9ad78225527028db113d72c0ec8c12f8"
		puts ""
		puts "Error:"
		reason.to_s.split("\n").each { |line|
			puts "\t#{line}"
		}

		return
	end

	if outputFile.is_a?(String)
		if File.exists?(outputFile)
			puts "Status: 200 OK"
			puts "Content-type: application/octet-stream"
			puts ""
			deliverFile(outputFile)

			return
		end
	end

	puts "Status: 400 Not OK"
	puts "Content-type: text/plain"
	puts ""
	puts "Something went terribly wrong"
end

private def httpInterface
	puts "No HTTP interface yet"

	exit 1
end

if ENV["GATEWAY_INTERFACE"]?
	cgiInterface
else
	httpInterface
end