From 6b75dfd6daa5af3438017dc05521dcef7d50d3e9 Mon Sep 17 00:00:00 2001 From: snow flurry Date: Wed, 24 Mar 2021 19:16:59 -0700 Subject: [PATCH] Initial commit --- LICENSE | 24 ++++++++++ README.md | 3 ++ src/Gemfile | 3 ++ src/aperture.rb | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ src/libmagic.rb | 118 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/Gemfile create mode 100644 src/aperture.rb create mode 100644 src/libmagic.rb diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..671bad7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2021 snow flurry. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..98dd071 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# aperture - Ruby/CGI script for file uploading + +TODO: docs diff --git a/src/Gemfile b/src/Gemfile new file mode 100644 index 0000000..f13afa1 --- /dev/null +++ b/src/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'ffi', '~> 1.9', '>= 1.9.10' diff --git a/src/aperture.rb b/src/aperture.rb new file mode 100644 index 0000000..9f843f3 --- /dev/null +++ b/src/aperture.rb @@ -0,0 +1,125 @@ +#!/usr/bin/env ruby + +## TODO: Can we somehow make this dynamic? +CONF_PATH = "/etc/aperture.conf" +USER_PREFIX = "~" + +require 'cgi' +require 'yaml' +require_relative 'libmagic' + +$cgi = CGI.new + +# convenience function to print an error to CGI log and quit +def die(status, error) + puts $cgi.http_header { + :type => "text/plain", + :status => status + } + if status == "SERVER_ERROR" + $stderr.puts "[aperture:fatal] " + error + puts "Something went wrong! Ask the server admin to check the logs for more info." + else + puts error + end + exit +end + +def get_yaml(path) + if not File.file? path + die "SERVER_ERROR", "Application not configured" + end + + begin + cfg = YAML.load( + File.open(path).read + ) + return cfg + rescue => + die "SERVER_ERROR", "Config file broken ;_;" + end + # should be unreachable + die "SERVER_ERROR", "get_yaml broken ;_;" +end + +# sanitizes string so all that's left is alphanumerics and . +def sanitize(str) + return str.gsub(/(^\.|[^0-9a-zA-Z\.])/i, '_') +end + +# main function +def main() + # consistency checks for request + # if not authenticated, ignore + if $cgi.REMOTE_USER.nil? || $cgi.REMOTE_USER.empty? + die "FORBIDDEN", "" + end + + # even though REMOTE_USER comes from the upstream, don't trust it + clean_name = sanitize $cgi.REMOTE_USER + + # Only POST supported for this API + if $cgi.REQUEST_METHOD != "POST" + die "METHOD_NOT_ALLOWED", "Method not allowed" + end + + if $cgi.CONTENT_TYPE != "multipart/form-data" + die "NOT_ACCEPTABLE", "Please use multipart/form-data" + end + + cfg = get_yaml CONF_PATH + + # make sure expected config entries are there + if cfg[:save_path].nil? or cfg[:save_path].empty? + die "SERVER_ERROR", "config: save_path not set!" + end + + if cfg[:root_url].nil? or cfg[:root_url].empty? + die "SERVER_ERROR", "config: root_url not set!" + end + + # XXX: configurable prefix? + user_dir = cfg[:save_path] + "/" + USER_PREFIX + clean_name + + if not File.directory? user_dir + die "SERVER_ERROR", "User directory " + user_dir + " doesn't exist!" + end + + # get file data + if $cgi.params['sendfile'].nil? || $cgi.params['sendfile'].empty? + die "NOT_ACCEPTABLE", "Missing sendfile?" + end + + upfile = $cgi.params['sendfile'][0] + + # get the mime type from the file contents, not HTTP + lm = LibMagic::Magic.new(cfg[:magic_path]) + ftype = lm.get_mime_type(upfile) + if ftype.match(/^(image|text|audio|video)\/[a-zA-Z0-9\-\.]+$/).nil? + die "NOT_ACCEPTABLE", "MIME type " + ftype + " not supported here" + end + lm.close + + clean_file = sanitize upfile.original_filename + full_path = user_dir + "/" + clean_file + + # don't overwrite an existing file unless they asked for it + if $cgi.params['overwrite'] != "true" and File.exists? full_path + die "NOT_ACCEPTABLE" "File " + clean_file + " already exists!" + end + + # finally, store the file + File.open(full_path, "w") do |new_file| + new_file.write(upfile.read) + end + + # print OK and send URL + + $cgi.print $cgi.http_header("status" => "OK", + "type" => "text/plain") + + $cgi.print cfg[:root_url] + USER_PREFIX + clean_name + "/" + clean_file +end + +main +exit 0 diff --git a/src/libmagic.rb b/src/libmagic.rb new file mode 100644 index 0000000..04ea77c --- /dev/null +++ b/src/libmagic.rb @@ -0,0 +1,118 @@ +require 'ffi' + +module LibMagic + extend FFI::Library + ffi_lib 'magic' + + ## constants + MAGIC_NONE = 0x0 + MAGIC_DEBUG = 0x1 + MAGIC_SYMLINK = 0x2 + MAGIC_COMPRESS = 0x4 + MAGIC_DEVICES = 0x8 + MAGIC_MIME_TYPE = 0x10 + MAGIC_CONTINUE = 0x20 + MAGIC_CHECK = 0x40 + MAGIC_PRESERVE_ATIME = 0x80 + MAGIC_RAW = 0x100 + MAGIC_ERROR = 0x200 + MAGIC_MIME_ENCODING = 0x400 + MAGIC_MIME = (MAGIC_MIME_TYPE | MAGIC_MIME_ENCODING) + MAGIC_APPLE = 0x800 + MAGIC_EXTENSION = 0x1000000 + MAGIC_COMPRESS_TRANSP = 0x2000000 + + # error used for handling libmagic errors + class MagicError < StandardError + def initialize(msg="Unknown error occurred", errno=0) + @errno = errno + super(msg) + end + end + + class Magic + # initialize libmagic + def initialize(db_path = nil, flags = LibMagic::MAGIC_MIME) + # nullptr for default db path + dbptr = FFI::Pointer.new 0x0 + @magic_inst = ffi_magic_open(flags) + if @magic_inst.null? + raise "magic_open() failed" + end + + # now that we have our cookie, clear it on exit + at_exit do + self.close + end + + # load magic database + if db_path is not nil + dbptr = db_path + end + + res = ffi_magic_load(@magic_inst, dbptr) + if res != 0 + self.throw_error + end + end + + # Gets MIME type for a given stream + def self.get_mime_type(stream) + res = nil + if stream.fileno.nil? # magic_buffer + buf = stream.string + buflen = buf.bytesize + res = ffi_magic_buffer(@magic_inst, buf, buflen) + else # magic_descriptor + res = ffi_magic_descriptor(@magic_inst, stream.fileno) + end + if res.nil? || res.null? + self.throw_error + else + retstr, retfree = FFI::StrPtrConverter.from_native(res, nil) + return retstr + end + # unreachable + return nil + end + + def self.close() + unless @magic_inst.nil? + ffi_magic_close(@magic_inst) + @magic_inst = nil + end + end + + private + + # convenience method for throwing MagicErrors + def self.throw_error() + if @magic_inst.null? + raise LibMagic::MagicError.new + else + errptr = ffi_magic_error(@magic_inst) + errno = ffi_magic_errno(@magic_inst) + errstr, errfree = FFI::StrPtrConverter.from_native(errptr, nil) + raise LibMagic::MagicError.new(errstr, errno) + end + end + + ## ffi + + # i/o open/close + attach_function :ffi_magic_open, :magic_open, [:int], :pointer + attach_function :ffi_magic_close, :magic_close, [:pointer], :void + # error info (a la GetLastError/win32) + attach_function :ffi_magic_error, :magic_error, [:pointer], :strptr + attach_function :ffi_magic_errno, :magic_errno, [:pointer], :int + # libmagic flags + attach_function :ffi_magic_getflags, :magic_getflags, [:pointer], :int + attach_function :ffi_magic_setflags, :magic_setflags, [:pointer, :int], :int + # load the libmagic db -- MUST BE DONE BEFORE GETTING MAGIC INFO + attach_function :ffi_magic_load, :magic_load, [:pointer, :string], :int + # get magic info + attach_function :ffi_magic_file, :magic_file, [:pointer, :string], :strptr + attach_function :ffi_magic_buffer, :magic_buffer, [:pointer, :pointer, :size_t], :strptr + attach_function :ffi_magic_descriptor, :magic_descriptor, [:pointer, :int], :strptr + end +end