Initial commit

This commit is contained in:
snow flurry 2021-03-24 19:16:59 -07:00
commit 6b75dfd6da
5 changed files with 273 additions and 0 deletions

24
LICENSE Normal file
View file

@ -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.

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# aperture - Ruby/CGI script for file uploading
TODO: docs

3
src/Gemfile Normal file
View file

@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'ffi', '~> 1.9', '>= 1.9.10'

125
src/aperture.rb Normal file
View file

@ -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

118
src/libmagic.rb Normal file
View file

@ -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