#! /usr/bin/env tclsh

set hashInfo {
	sha1 {
		length 20
		command "openssl sha1"
	}
	sha256 {
		length 32
		command "openssl sha256"
	}
}

proc openssl {command filename} {
	catch {
		set output [exec openssl $command $filename]
	}

	if {![info exists output]} {
		return ""
	}

	set output [split $output =]
	set output [string trim [lindex $output end]]

	return $output
}

proc createURL {} {
	foreach hostEntry [list HTTP_HOST SERVER_NAME] {
		if {[info exists ::env($hostEntry)]} {
			set host $::env($hostEntry)

			break
		}
	}

	if {![info exists host]} {
		return ""
	}

	if {[info exists ::env(HTTPS)] && $::env(HTTPS) == "on"} {
		set scheme "https"
	} else {
		set scheme "http"
	}

	if {[info exists ::env(SERVER_PORT)]} {
		switch -- $scheme {
			"http" { set defaultPort 80 }
			"https" { set defaultPort 443 }
		}

		if {$::env(SERVER_PORT) != $defaultPort} {
			set addPort $::env(SERVER_PORT)
		}
	}

	if {[info exists addPort]} {
		set host "${host}:${addPort}"
	}

	set url "${scheme}://${host}/"

	return $url
}

proc validationFailure {shortreason args} {
	puts "Content-type: text/plain"

	puts "Status: 400 $shortreason"

	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"

	if {[llength $args] != 0} {
		puts ""
		puts "Error:"
		foreach arg $args {
			puts "\t$arg"
		}
	}

	exit 0
}

proc sendUserFile {filename} {
	if {[catch {
		set inFd [open $filename r]
	}]} {
		return false
	}

	fconfigure $inFd -translation binary

	puts "Content-type: application/octet-stream"
	puts ""

	flush stdout

	fconfigure stdout -translation binary

	fcopy $inFd stdout

	close $inFd

	return true
}

proc validateHash {filename command value} {
	lappend command $filename

	set chkValue [string tolower [uplevel #0 $command]]

	if {$chkValue == $value} {
		return true
	}

	return false
}

proc cacheRemoteURL {url cachefile hashCommand hashValue} {
	set retval false

	set tmpfile "${cachefile}-[expr rand()]-[clock clicks]"

	file mkdir [file dirname $tmpfile]

	catch {
		set outChan [open $tmpfile w]
	}

	if {![info exists outChan]} {
		return false
	}

	fconfigure $outChan -translation binary

	if {[catch {
		exec curl -sSkL -- $url >@ $outChan
	} err]} {
		set result "500"
		set resultString $err
	} else {
		set result "200"
		set resultString "Success"
	}

	close $outChan

	if {$result == 200} {
		if {[validateHash $tmpfile $hashCommand $hashValue]} {
			set retval true
			set reason "Success"

			file rename -force -- $tmpfile $cachefile
		} else {
			set reason "Hash validation failure"
		}
	} else {
		set reason "Fetching remote URL returned non-200: $result $resultString"
	}

	catch {
		file delete -force -- $tmpfile
	}

	return [list $retval $reason]
}

set baseDir $::env(DOCUMENT_ROOT)
set requestPath $::env(REQUEST_URI)

set work [split $requestPath /]
set hashMethod [lindex $work 1]
set hashValue [lindex $work 2]

set targetFile [file join $baseDir $hashMethod $hashValue]

if {![dict exists $hashInfo $hashMethod]} {
	validationFailure "Invalid Hashing Mechanism" \
		"Invalid hashing mechanism specified, must be one of: [join [dict keys $hashInfo] {, }]" \
		"Got: \"${hashMethod}\""
}

set hashLength [dict get $hashInfo $hashMethod length]
set hashCommand [dict get $hashInfo $hashMethod command]

if {
	[string length $hashValue] != [expr {$hashLength * 2}]
		|| 
	![regexp {^[0-9a-f]*$} $hashValue]
} {
	validationFailure "Hash Value Specified" \
		"Invalid hash value specified -- wrong length.  The hash value supplied is expected to be a hex string of [expr {$hashLength * 2}] characters 0-9a-f" \
		"Got: \"${hashValue}\""
}

if {[file exists $targetFile]} {
	if {[sendUserFile $targetFile]} {
		exit 0
	}
}

if {![info exists ::env(HTTP_X_CACHE_URL)]} {
	validationFailure "Missing X-Cache-URL Header" \
		"No X-Cache-URL header found, we don't know what to cache and this hash does not already exist."
}

set originURL $::env(HTTP_X_CACHE_URL)

switch -glob -- $originURL {
	"http://*" - "https://*" - "ftp://*" {}
	default {
		validationFailure "Invalid URL Scheme" \
			 "We only support HTTP, HTTPS, and FTP transports for origin URLs.  The URL must begin with one of: http://, https://, ftp://"
	}
}

set cacheResult [cacheRemoteURL $originURL $targetFile $hashCommand $hashValue]
set cacheResultStatus [lindex $cacheResult 0]
set cacheResultReason [lindex $cacheResult 1]

if {$cacheResultStatus} {
	if {[sendUserFile $targetFile]} {
		exit 0
	}

	validationFailure "Failed open fetched file" \
		"We fetched the file, but we were not able to open afterwards.  Something has gone horribly wrong."
}

validationFailure "Failed to fetch remote file" \
	"We could not fetch the remote file: $cacheResultReason"
