Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
7cc2ad9117 | |||
44d7afddfc | |||
c0f73bab0a | |||
b65acac27d | |||
b19c423648 |
20 changed files with 265 additions and 337 deletions
|
@ -1,8 +1,7 @@
|
|||
name: 'File-uploader-crystal CI'
|
||||
|
||||
on:
|
||||
# workflow_dispatch:
|
||||
# inputs: {}
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 7 * * 0'
|
||||
push:
|
||||
|
@ -28,14 +27,14 @@ jobs:
|
|||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
uses: https://github.com/docker/metadata-action@v6
|
||||
with:
|
||||
images: git.nadeko.net/fijxu/file-uploader-crystal
|
||||
tags: |
|
||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- uses: https://code.forgejo.org/docker/build-push-action@v5
|
||||
- uses: https://code.forgejo.org/docker/build-push-action@v6
|
||||
name: Build images
|
||||
with:
|
||||
context: .
|
||||
|
@ -43,5 +42,5 @@ jobs:
|
|||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
"release=1"
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -7,4 +7,5 @@ data
|
|||
torexitnodes.txt
|
||||
files
|
||||
thumbnails
|
||||
db.sqlite3
|
||||
db.sqlite3
|
||||
config/config.yml
|
23
Dockerfile
23
Dockerfile
|
@ -1,11 +1,10 @@
|
|||
# Based on https://github.com/iv-org/invidious/blob/master/docker/Dockerfile
|
||||
FROM crystallang/crystal:1.14.0-alpine AS builder
|
||||
FROM crystallang/crystal:1.16.1-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache sqlite-static yaml-static
|
||||
|
||||
ARG release
|
||||
WORKDIR /file-uploader-crystal
|
||||
|
||||
WORKDIR /file-uploader-crystal
|
||||
COPY ./shard.yml ./shard.yml
|
||||
COPY ./shard.lock ./shard.lock
|
||||
RUN shards install --production
|
||||
|
@ -15,21 +14,29 @@ COPY ./src/ ./src/
|
|||
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||
COPY ./.git/ ./.git/
|
||||
|
||||
RUN crystal build ./src/file-uploader-crystal.cr \
|
||||
RUN --mount=type=cache,target=/root/.cache/crystal \
|
||||
crystal build ./src/file-uploader-crystal.cr \
|
||||
--release \
|
||||
--static --warnings all
|
||||
|
||||
FROM alpine:3.20
|
||||
# 2nd stage
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache tini ffmpeg
|
||||
|
||||
WORKDIR /file-uploader-crystal
|
||||
|
||||
RUN addgroup -g 1000 -S file-uploader-crystal && \
|
||||
adduser -u 1000 -S file-uploader-crystal -G file-uploader-crystal
|
||||
COPY --chown=file-uploader-crystal ./config/config.* ./config/
|
||||
|
||||
COPY --chown=file-uploader-crystal ./config/config.example.yml ./config/
|
||||
RUN mv -n config/config.example.yml config/config.yml
|
||||
COPY --from=builder /file-uploader-crystal/file-uploader-crystal .
|
||||
RUN chmod o+rX -R ./config
|
||||
COPY --from=builder /file-uploader-crystal/file-uploader-crystal /file-uploader-crystal
|
||||
RUN chmod o+rX -R /file-uploader-crystal/file-uploader-crystal
|
||||
RUN chown file-uploader-crystal: -R /file-uploader-crystal
|
||||
|
||||
EXPOSE 8080
|
||||
USER file-uploader-crystal
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
|
||||
CMD [ "/file-uploader-crystal/file-uploader-crystal" ]
|
||||
|
|
|
@ -1,46 +1,43 @@
|
|||
colorize_logs: true
|
||||
log_level: "debug"
|
||||
|
||||
# File paths
|
||||
files: "./files"
|
||||
thumbnails: "./thumbnails"
|
||||
db: "./db.sqlite3"
|
||||
|
||||
# Tor
|
||||
block_tor_addresses: true
|
||||
tor_exit_nodes_check: 1600
|
||||
tor_exit_nodes_url: "https://check.torproject.org/exit-addresses"
|
||||
tor_message: "TOR IS BLOCKED!"
|
||||
|
||||
generate_thumbnails: true
|
||||
db: "./db/db.sqlite3"
|
||||
adminEnabled: true
|
||||
adminApiKey: "asd"
|
||||
filename_length: 3
|
||||
# In MiB
|
||||
admin_enabled: true
|
||||
admin_api_key: "asd"
|
||||
size_limit: 512
|
||||
enable_checksums: false
|
||||
port: 8080
|
||||
blockTorAddresses: true
|
||||
# Every hour
|
||||
torExitNodesCheck: 1600
|
||||
torExitNodesUrl: "https://check.torproject.org/exit-addresses"
|
||||
torExitNodesFile: "./torexitnodes.txt"
|
||||
torMessage: "TOR IS BLOCKED!"
|
||||
# Set this to 0 to disable rate limiting
|
||||
filesPerIP: 2
|
||||
rateLimitPeriod: 20
|
||||
rateLimitMessage: ""
|
||||
# If you define the unix socket, it will only listen on the socket and not the port.
|
||||
#unix_socket: "/tmp/file-uploader.sock"
|
||||
# In days
|
||||
deleteFilesAfter: 7
|
||||
# In seconds
|
||||
deleteFilesCheck: 1600
|
||||
deleteKeyLength: 4
|
||||
siteInfo: "Whatever you want to put here"
|
||||
siteWarning: "WARNING!"
|
||||
log_level: "debug"
|
||||
|
||||
blockedExtensions:
|
||||
files_per_ip: 2
|
||||
rate_limit_period: 20
|
||||
rate_limit_message: ""
|
||||
filename_length: 3
|
||||
delete_files_after: 7
|
||||
delete_files_check: 1800
|
||||
delete_key_length: 4
|
||||
|
||||
site_info: "Whatever you want to put here"
|
||||
site_warning: "WARNING!"
|
||||
|
||||
blocked_extensions:
|
||||
- "exe"
|
||||
|
||||
# List of useragents that use OpenGraph to gather file information
|
||||
opengraphUseragents:
|
||||
opengraph_useragents:
|
||||
- "chatterino-api-cache/"
|
||||
- "FFZBot/"
|
||||
- "Twitterbot/"
|
||||
- "Synapse/"
|
||||
- "Mastodon/"
|
||||
|
||||
# You can leave it empty, or add your own domains.
|
||||
alternative_domains:
|
||||
- "example.com"
|
||||
# alternative_domains:
|
||||
# - "example.com"
|
||||
|
|
|
@ -23,10 +23,10 @@ class Config
|
|||
property db : String = "./db.sqlite3"
|
||||
|
||||
# Enable or disable the admin API
|
||||
property adminEnabled : Bool = false
|
||||
property admin_enabled : Bool = false
|
||||
# The API key for admin routes. It's passed as a "X-Api-Key" header to the
|
||||
# request
|
||||
property adminApiKey : String? = ""
|
||||
property admin_api_key : String? = nil
|
||||
|
||||
# Not implemented
|
||||
property incrementalfilename_length : Bool = true
|
||||
|
@ -34,6 +34,7 @@ class Config
|
|||
property filename_length : Int32 = 3
|
||||
# In MiB
|
||||
property size_limit : Int16 = 512
|
||||
property enable_checksums : Bool = true
|
||||
|
||||
# A file path where do you want to place a unix socket (THIS WILL DISABLE ACCESS
|
||||
# BY IP ADDRESS)
|
||||
|
@ -41,56 +42,49 @@ class Config
|
|||
|
||||
# True if you want this program to block IP addresses coming from the Tor
|
||||
# network
|
||||
property blockTorAddresses : Bool? = false
|
||||
property block_tor_addresses : Bool = false
|
||||
# How often (in seconds) should this program download the exit nodes list
|
||||
property torExitNodesCheck : Int32 = 3600
|
||||
property tor_exit_nodes_check : Int32 = 3600
|
||||
# Only https://check.torproject.org/exit-addresses is supported
|
||||
property torExitNodesUrl : String = "https://check.torproject.org/exit-addresses"
|
||||
property tor_exit_nodes_url : String = "https://check.torproject.org/exit-addresses"
|
||||
# Message that will be displayed to the Tor user.
|
||||
# It will be shown on the Frontend and shown in the error 401 when a user
|
||||
# tries to upload a file using curl or any other tool
|
||||
property torMessage : String? = "Tor is blocked!"
|
||||
property tor_message : String? = "Tor is blocked!"
|
||||
|
||||
# How many files an IP address can upload to the server
|
||||
property filesPerIP : Int32 = 32
|
||||
# How many files an IP address can upload to the server. Setting this to 0
|
||||
# disables rate limits in the rate limit period
|
||||
property files_per_ip : Int32 = 32
|
||||
# How often is the file limit per IP reset? (in seconds)
|
||||
property rateLimitPeriod : Int32 = 600
|
||||
property rate_limit_period : Int32 = 600
|
||||
# TODO: UNUSED CONSTANT
|
||||
property rateLimitMessage : String = ""
|
||||
property rate_limit_message : String = ""
|
||||
|
||||
# Delete the files after how many days?
|
||||
property deleteFilesAfter : Int32 = 14
|
||||
property delete_files_after : Int32 = 14
|
||||
# How often should the check of old files be performed? (in seconds)
|
||||
property deleteFilesCheck : Int32 = 1800
|
||||
property delete_files_check : Int32 = 1800
|
||||
# The lenght of the delete key
|
||||
property deleteKeyLength : Int32 = 6
|
||||
property delete_key_length : Int32 = 6
|
||||
|
||||
property siteInfo : String = "xd"
|
||||
property site_info : String = "xd"
|
||||
# TODO: UNUSED CONSTANT
|
||||
property siteWarning : String? = ""
|
||||
property site_warning : String? = ""
|
||||
|
||||
# Blocked extensions that are not allowed to be uploaded to the server
|
||||
property blockedExtensions : Array(String) = [] of String
|
||||
property blocked_extensions : Array(String) = [] of String
|
||||
|
||||
# A list of OpenGraph user agents. If the request contains one of those User
|
||||
# agents when trying to retrieve a file from the server; the server will
|
||||
# reply with an HTML with OpenGraph tags, pointing to the media thumbnail
|
||||
# (if it was generated successfully) and the name of the file as title
|
||||
property opengraphUseragents : Array(String) = [] of String
|
||||
property opengraph_useragents : Array(String) = [] of String
|
||||
|
||||
# Since this program detects the Host header of the client it can be used
|
||||
# with multiple domains. You can display the domains in the frontend
|
||||
# and in `/api/stats`
|
||||
property alternative_domains : Array(String) = [] of String
|
||||
|
||||
def self.load
|
||||
config_file = "config/config.yml"
|
||||
config_yaml = File.read(config_file)
|
||||
config = Config.from_yaml(config_yaml)
|
||||
check_config(config)
|
||||
config
|
||||
end
|
||||
|
||||
def self.check_config(config : Config)
|
||||
if config.filename_length <= 0
|
||||
puts "Config: filename_length cannot be less or equal to 0"
|
||||
|
@ -104,4 +98,11 @@ class Config
|
|||
config.thumbnails = config.thumbnails.chomp('/')
|
||||
end
|
||||
end
|
||||
|
||||
def self.load(config_file : String = "config/config.yml")
|
||||
config_yaml = File.read(config_file)
|
||||
config = Config.from_yaml(config_yaml)
|
||||
check_config(config)
|
||||
config
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,9 +64,9 @@ module Database::Files
|
|||
|
||||
def old_files : Array(UFile)
|
||||
request = <<-SQL
|
||||
SELECT filename, extension, thumbnail
|
||||
SELECT *
|
||||
FROM files
|
||||
WHERE uploaded_at < strftime('%s', 'now') - #{CONFIG.deleteFilesAfter * 3600}
|
||||
WHERE uploaded_at < strftime('%s', 'now') - #{CONFIG.delete_files_after * 3600}
|
||||
SQL
|
||||
|
||||
SQL.query_all(request, as: UFile)
|
||||
|
|
|
@ -1,16 +1,55 @@
|
|||
module Database::IP
|
||||
extend self
|
||||
|
||||
# -------------------
|
||||
# Insert / Delete
|
||||
# -------------------
|
||||
|
||||
def insert(ip : IP) : Nil
|
||||
def insert(ip : UIP) : DB::ExecResult
|
||||
request = <<-SQL
|
||||
INSERT OR IGNORE
|
||||
INTO ips (ip, date)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
INTO ips
|
||||
VALUES ($1, $2, $3)
|
||||
SQL
|
||||
|
||||
SQL.exec(request, *ip.to_tuple)
|
||||
end
|
||||
|
||||
def delete(ip : String) : Nil
|
||||
request = <<-SQL
|
||||
DELETE
|
||||
FROM ips
|
||||
WHERE ip = ?
|
||||
SQL
|
||||
|
||||
SQL.exec(request, ip)
|
||||
end
|
||||
|
||||
# -------------------
|
||||
# Select
|
||||
# -------------------
|
||||
|
||||
def select(ip : String) : UIP?
|
||||
request = <<-SQL
|
||||
SELECT *
|
||||
FROM ips
|
||||
WHERE ip = ?
|
||||
SQL
|
||||
|
||||
SQL.query_one?(request, ip, as: UIP)
|
||||
end
|
||||
|
||||
# -------------------
|
||||
# Update
|
||||
# -------------------
|
||||
|
||||
def increase_count(ip : UIP) : Nil
|
||||
request = <<-SQL
|
||||
UPDATE ips
|
||||
SET count = count + 1
|
||||
WHERE ip = $1
|
||||
SQL
|
||||
|
||||
SQL.exec(request, ip.ip)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
# Pretty cool way to write background jobs! :)
|
||||
module Jobs
|
||||
def self.check_old_files
|
||||
if CONFIG.deleteFilesCheck <= 0
|
||||
if CONFIG.delete_files_check <= 0
|
||||
LOGGER.info "File deletion is disabled"
|
||||
return
|
||||
end
|
||||
spawn do
|
||||
loop do
|
||||
Utils.check_old_files
|
||||
sleep CONFIG.deleteFilesCheck.seconds
|
||||
sleep CONFIG.delete_files_check.seconds
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.retrieve_tor_exit_nodes
|
||||
if !CONFIG.blockTorAddresses
|
||||
if !CONFIG.block_tor_addresses
|
||||
return
|
||||
end
|
||||
LOGGER.info("Blocking Tor exit nodes")
|
||||
spawn do
|
||||
loop do
|
||||
Utils::Tor.refresh_exit_nodes
|
||||
sleep CONFIG.torExitNodesCheck.seconds
|
||||
sleep CONFIG.tor_exit_nodes_check.seconds
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,14 +13,14 @@ end
|
|||
|
||||
module Headers
|
||||
macro host
|
||||
env.request.headers["X-Forwarded-Host"]?
|
||||
env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]?
|
||||
end
|
||||
|
||||
macro scheme
|
||||
env.request.headers["X-Forwarded-Proto"]?
|
||||
env.request.headers["X-Forwarded-Proto"]? || "http"
|
||||
end
|
||||
|
||||
macro ip_addr
|
||||
env.request.headers["X-Real-IP"]?
|
||||
env.request.headers["X-Real-IP"]? || env.request.remote_address.as?(Socket::IPAddress).try &.address
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,7 +5,7 @@ module Routes::Deletion
|
|||
key = env.params.query["key"]?
|
||||
|
||||
if !key || key.empty?
|
||||
ee 400, "No delete key suplied"
|
||||
ee 400, "No delete key supplied"
|
||||
end
|
||||
|
||||
file = Database::Files.select_with_key(key)
|
||||
|
|
|
@ -18,7 +18,7 @@ module Routing::Misc
|
|||
property alternative_domains : Array(String)
|
||||
|
||||
def initialize
|
||||
@files_hosted = SQL.query_one("SELECT COUNT (filename) FROM files", as: Int32)
|
||||
@files_hosted = Database::Files.file_count
|
||||
@max_upload_size = CONFIG.size_limit.to_s
|
||||
@thumbnail_generation = CONFIG.generate_thumbnails
|
||||
@filename_length = CONFIG.filename_length
|
||||
|
|
|
@ -4,7 +4,7 @@ module Routes::Retrieve
|
|||
def retrieve_file(env)
|
||||
host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]?
|
||||
scheme = env.request.headers["X-Forwarded-Proto"]? || "http"
|
||||
ip_addr = env.request.headers["X-Real-IP"]? || env.request.remote_address
|
||||
ip_addr = env.request.headers["X-Real-IP"]? || env.request.remote_address.as?(Socket::IPAddress).try &.address
|
||||
filename = env.params.url["filename"].split(".").first
|
||||
|
||||
begin
|
||||
|
@ -20,23 +20,19 @@ module Routes::Retrieve
|
|||
env.response.headers["Content-Disposition"] = "inline; filename*=UTF-8''#{file.original_filename}"
|
||||
env.response.headers["ETag"] = "#{file.checksum}"
|
||||
|
||||
CONFIG.opengraphUseragents.each do |useragent|
|
||||
CONFIG.opengraph_useragents.each do |useragent|
|
||||
env.response.content_type = "text/html"
|
||||
|
||||
if env.request.headers.["User-Agent"]?.try &.includes?(useragent)
|
||||
return %(
|
||||
<!DOCTYPE html>
|
||||
if env.request.headers["User-Agent"]?.try &.includes?(useragent)
|
||||
return %(<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta property="og:title" content="#{file.filename}">
|
||||
<meta property="og:title" content="#{file.original_filename}">
|
||||
<meta property="og:url" content="#{scheme}://#{host}/#{file.filename}">
|
||||
#{if file.thumbnail
|
||||
%(<meta property="og:image" content="#{scheme}://#{host}/thumbnail/#{file.filename}.jpg">)
|
||||
end}
|
||||
</head>
|
||||
</html>
|
||||
)
|
||||
#{%(<meta property="og:image" content="#{scheme}://#{host}/thumbnail/#{file.filename}.jpg">) if file.thumbnail}
|
||||
</head>
|
||||
</html>)
|
||||
end
|
||||
end
|
||||
send_file env, "#{CONFIG.files}/#{file.filename}#{file.extension}"
|
||||
|
@ -44,7 +40,6 @@ module Routes::Retrieve
|
|||
|
||||
def retrieve_thumbnail(env)
|
||||
thumbnail = env.params.url["thumbnail"]?
|
||||
pp "#{CONFIG.thumbnails}/#{thumbnail}"
|
||||
|
||||
begin
|
||||
send_file env, "#{CONFIG.thumbnails}/#{thumbnail}"
|
||||
|
|
|
@ -10,7 +10,7 @@ module Routes::Upload
|
|||
property id : String
|
||||
property ext : String
|
||||
property name : String
|
||||
property checksum : String
|
||||
property checksum : String?
|
||||
@[JSON::Field(key: "deleteKey")]
|
||||
property delete_key : String
|
||||
@[JSON::Field(key: "deleteLink")]
|
||||
|
@ -31,7 +31,7 @@ module Routes::Upload
|
|||
def upload(env)
|
||||
host = env.request.headers["X-Forwarded-Host"]? || env.request.headers["Host"]?
|
||||
scheme = env.request.headers["X-Forwarded-Proto"]? || "http"
|
||||
ip_addr = env.request.headers["X-Real-IP"]? || env.request.remote_address
|
||||
ip_addr = env.request.headers["X-Real-IP"]? || env.request.remote_address.as?(Socket::IPAddress).try &.address
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
# You can modify this if you want to allow files smaller than 1MiB.
|
||||
|
@ -46,6 +46,7 @@ module Routes::Upload
|
|||
end
|
||||
|
||||
file = UFile.new
|
||||
ip = UIP.new
|
||||
|
||||
HTTP::FormData.parse(env.request) do |upload|
|
||||
upload_filename = upload.filename
|
||||
|
@ -57,12 +58,17 @@ module Routes::Upload
|
|||
ee 403, "No file provided"
|
||||
end
|
||||
|
||||
file.extension = File.extname("#{upload.filename}")
|
||||
file.filename = Utils.generate_filename
|
||||
|
||||
if file.original_filename == "control_v.png"
|
||||
file.original_filename = file.filename
|
||||
end
|
||||
|
||||
file.extension = File.extname("#{upload.filename}")
|
||||
full_filename = file.filename + file.extension
|
||||
file_path = "#{CONFIG.files}/#{full_filename}"
|
||||
|
||||
if CONFIG.blockedExtensions.includes?(file.extension.split(".")[1])
|
||||
if CONFIG.blocked_extensions.includes?(file.extension.split(".")[1])
|
||||
ee 401, "Extension '#{file.extension}' is not allowed"
|
||||
end
|
||||
|
||||
|
@ -70,16 +76,21 @@ module Routes::Upload
|
|||
IO.copy(upload.body, output)
|
||||
end
|
||||
|
||||
file.uploaded_at = Time.utc.to_unix.to_s
|
||||
file.checksum = Utils::Hashing.hash_file(file_path)
|
||||
file.uploaded_at = Time.utc.to_unix
|
||||
|
||||
if CONFIG.enable_checksums
|
||||
file.checksum = Utils::Hashing.hash_file(file_path)
|
||||
end
|
||||
end
|
||||
|
||||
if CONFIG.deleteKeyLength > 0
|
||||
file.delete_key = Random.base58(CONFIG.deleteKeyLength)
|
||||
file.ip = ip_addr.to_s
|
||||
ip.ip = file.ip
|
||||
ip.date = file.uploaded_at
|
||||
|
||||
if CONFIG.delete_key_length > 0
|
||||
file.delete_key = Random.base58(CONFIG.delete_key_length)
|
||||
end
|
||||
|
||||
# X-Real-IP if behind a reverse proxy and the header is set in the reverse
|
||||
# proxy configuration.
|
||||
begin
|
||||
spawn { Utils.generate_thumbnail(file.filename, file.extension) }
|
||||
rescue ex
|
||||
|
@ -88,10 +99,8 @@ module Routes::Upload
|
|||
|
||||
begin
|
||||
Database::Files.insert(file)
|
||||
# Database::IP.insert(ip_addr)
|
||||
# SQL.exec "INSERT OR IGNORE INTO ips (ip, date) VALUES (?, ?)", ip_address, Time.utc.to_unix
|
||||
# # SQL.exec "INSERT OR IGNORE INTO ips (ip) VALUES ('#{ip_address}')"
|
||||
# SQL.exec "UPDATE ips SET count = count + 1 WHERE ip = ('#{ip_address}')"
|
||||
exists = Database::IP.insert(ip).rows_affected == 0
|
||||
Database::IP.increase_count(ip) if exists
|
||||
rescue ex
|
||||
LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}"
|
||||
ee 500, "An error ocurred when trying to insert the data into the DB"
|
||||
|
@ -100,180 +109,4 @@ module Routes::Upload
|
|||
res = Response.new(file, scheme, host)
|
||||
res.to_json
|
||||
end
|
||||
|
||||
# The most unoptimized and unstable feature lol
|
||||
# def upload_url_bulk(env)
|
||||
# env.response.content_type = "application/json"
|
||||
# ip_address = Utils.ip_address(env)
|
||||
# protocol = Utils.protocol(env)
|
||||
# host = Utils.host(env)
|
||||
# begin
|
||||
# files = env.params.json["files"].as((Array(JSON::Any)))
|
||||
# rescue ex : JSON::ParseException
|
||||
# LOGGER.error "Body malformed: #{ex.message}"
|
||||
# ee 400, "Body malformed: #{ex.message}"
|
||||
# rescue ex
|
||||
# LOGGER.error "Unknown error: #{ex.message}"
|
||||
# ee 500, "Unknown error"
|
||||
# end
|
||||
# successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil)
|
||||
# failed_files = [] of String
|
||||
# # X-Real-IP if behind a reverse proxy and the header is set in the reverse
|
||||
# # proxy configuration.
|
||||
# files.each do |url|
|
||||
# url = url.to_s
|
||||
# filename = Utils.generate_filename
|
||||
# original_filename = ""
|
||||
# extension = ""
|
||||
# checksum = ""
|
||||
# uploaded_at = Time.utc
|
||||
# extension = File.extname(URI.parse(url).path)
|
||||
# if CONFIG.deleteKeyLength > 0
|
||||
# delete_key = Random.base58(CONFIG.deleteKeyLength)
|
||||
# end
|
||||
# file_path = "#{CONFIG.files}/#{filename}#{extension}"
|
||||
# File.open(file_path, "w") do |output|
|
||||
# begin
|
||||
# HTTP::Client.get(url) do |res|
|
||||
# IO.copy(res.body_io, output)
|
||||
# end
|
||||
# rescue ex
|
||||
# LOGGER.debug "Failed to download file '#{url}': #{ex.message}"
|
||||
# ee 403, "Failed to download file '#{url}'"
|
||||
# failed_files << url
|
||||
# end
|
||||
# end
|
||||
# # successfull_files << url
|
||||
# # end
|
||||
# if extension.empty?
|
||||
# extension = Utils.detect_extension(file_path)
|
||||
# File.rename(file_path, file_path + extension)
|
||||
# file_path = "#{CONFIG.files}/#{filename}#{extension}"
|
||||
# end
|
||||
# # The second one is faster and it uses less memory
|
||||
# # original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last
|
||||
# original_filename = url.split("/").last
|
||||
# checksum = Utils::Hashing.hash_file(file_path)
|
||||
# begin
|
||||
# spawn { Utils.generate_thumbnail(filename, extension) }
|
||||
# rescue ex
|
||||
# LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}"
|
||||
# end
|
||||
# begin
|
||||
# # Insert SQL data just before returning the upload information
|
||||
# SQL.exec("INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
# original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil)
|
||||
# successfull_files << {filename: filename,
|
||||
# original_filename: original_filename,
|
||||
# extension: extension,
|
||||
# delete_key: delete_key,
|
||||
# checksum: checksum}
|
||||
# rescue ex
|
||||
# LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}"
|
||||
# ee 500, "An error ocurred when trying to insert the data into the DB"
|
||||
# end
|
||||
# end
|
||||
# json = JSON.build do |j|
|
||||
# j.array do
|
||||
# successfull_files.each do |fileinfo|
|
||||
# j.object do
|
||||
# j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}"
|
||||
# j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}"
|
||||
# j.field "id", fileinfo[:filename]
|
||||
# j.field "ext", fileinfo[:extension]
|
||||
# j.field "name", fileinfo[:original_filename]
|
||||
# j.field "checksum", fileinfo[:checksum]
|
||||
# if CONFIG.deleteKeyLength > 0
|
||||
# delete_key = Random.base58(CONFIG.deleteKeyLength)
|
||||
# j.field "deleteKey", fileinfo[:delete_key]
|
||||
# j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# json
|
||||
# end
|
||||
|
||||
# def upload_url(env)
|
||||
# env.response.content_type = "application/json"
|
||||
# ip_address = Utils.ip_address(env)
|
||||
# protocol = Utils.protocol(env)
|
||||
# host = Utils.host(env)
|
||||
# url = env.params.query["url"]
|
||||
# successfull_files = [] of NamedTuple(filename: String, extension: String, original_filename: String, checksum: String, delete_key: String | Nil)
|
||||
# failed_files = [] of String
|
||||
# # X-Real-IP if behind a reverse proxy and the header is set in the reverse
|
||||
# # proxy configuration.
|
||||
# filename = Utils.generate_filename
|
||||
# original_filename = ""
|
||||
# extension = ""
|
||||
# checksum = ""
|
||||
# uploaded_at = Time.utc
|
||||
# extension = File.extname(URI.parse(url).path)
|
||||
# if CONFIG.deleteKeyLength > 0
|
||||
# delete_key = Random.base58(CONFIG.deleteKeyLength)
|
||||
# end
|
||||
# file_path = "#{CONFIG.files}/#{filename}#{extension}"
|
||||
# File.open(file_path, "w") do |output|
|
||||
# begin
|
||||
# # TODO: Connect timeout to prevent possible Denial of Service to the external website spamming requests
|
||||
# # https://crystal-lang.org/api/1.13.2/HTTP/Client.html#connect_timeout
|
||||
# HTTP::Client.get(url) do |res|
|
||||
# IO.copy(res.body_io, output)
|
||||
# end
|
||||
# rescue ex
|
||||
# LOGGER.debug "Failed to download file '#{url}': #{ex.message}"
|
||||
# ee 403, "Failed to download file '#{url}': #{ex.message}"
|
||||
# failed_files << url
|
||||
# end
|
||||
# end
|
||||
# if extension.empty?
|
||||
# extension = Utils.detect_extension(file_path)
|
||||
# File.rename(file_path, file_path + extension)
|
||||
# file_path = "#{CONFIG.files}/#{filename}#{extension}"
|
||||
# end
|
||||
# # The second one is faster and it uses less memory
|
||||
# # original_filename = URI.parse("https://ayaya.beauty/PqC").path.split("/").last
|
||||
# original_filename = url.split("/").last
|
||||
# checksum = Utils::Hashing.hash_file(file_path)
|
||||
# begin
|
||||
# spawn { Utils.generate_thumbnail(filename, extension) }
|
||||
# rescue ex
|
||||
# LOGGER.error "An error ocurred when trying to generate a thumbnail: #{ex.message}"
|
||||
# end
|
||||
# begin
|
||||
# # Insert SQL data just before returning the upload information
|
||||
# SQL.exec("INSERT INTO files VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
# original_filename, filename, extension, uploaded_at, checksum, ip_address, delete_key, nil)
|
||||
# successfull_files << {filename: filename,
|
||||
# original_filename: original_filename,
|
||||
# extension: extension,
|
||||
# delete_key: delete_key,
|
||||
# checksum: checksum}
|
||||
# rescue ex
|
||||
# LOGGER.error "An error ocurred when trying to insert the data into the DB: #{ex.message}"
|
||||
# ee 500, "An error ocurred when trying to insert the data into the DB"
|
||||
# end
|
||||
# json = JSON.build do |j|
|
||||
# j.array do
|
||||
# successfull_files.each do |fileinfo|
|
||||
# j.object do
|
||||
# j.field "link", "#{protocol}://#{host}/#{fileinfo[:filename]}"
|
||||
# j.field "linkExt", "#{protocol}://#{host}/#{fileinfo[:filename]}#{fileinfo[:extension]}"
|
||||
# j.field "id", fileinfo[:filename]
|
||||
# j.field "ext", fileinfo[:extension]
|
||||
# j.field "name", fileinfo[:original_filename]
|
||||
# j.field "checksum", fileinfo[:checksum]
|
||||
# if CONFIG.deleteKeyLength > 0
|
||||
# delete_key = Random.base58(CONFIG.deleteKeyLength)
|
||||
# j.field "deleteKey", fileinfo[:delete_key]
|
||||
# j.field "deleteLink", "#{protocol}://#{host}/delete?key=#{fileinfo[:delete_key]}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# json
|
||||
# end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module Routing
|
|||
# before_post "/api/admin/*" do |env|
|
||||
# env.response.content_type = "application/json"
|
||||
|
||||
# if env.request.headers.try &.["X-Api-Key"]? != CONFIG.adminApiKey || nil
|
||||
# if env.request.headers.try &.["X-Api-Key"]? != CONFIG.admin_api_key || nil
|
||||
# halt env, status_code: 401, response: "Wrong API Key"
|
||||
# end
|
||||
# end
|
||||
|
@ -31,34 +31,36 @@ module Routing
|
|||
api_key = env.request.headers["X-Api-Key"]?
|
||||
|
||||
# Skips Tor blocking and Rate limits if the API key matches
|
||||
if api_key == CONFIG.adminApiKey
|
||||
if api_key == CONFIG.admin_api_key
|
||||
next
|
||||
end
|
||||
|
||||
if CONFIG.blockTorAddresses && tor_exit_nodes.includes?(Headers.ip_addr)
|
||||
halt env, status_code: 401, response: CONFIG.torMessage
|
||||
if CONFIG.block_tor_addresses && tor_exit_nodes.includes?(Headers.ip_addr)
|
||||
halt env, status_code: 401, response: CONFIG.tor_message
|
||||
end
|
||||
end
|
||||
|
||||
before_post "/upload" do |env|
|
||||
begin
|
||||
ip_info = SQL.query_one?("SELECT ip, count, date FROM ips WHERE ip = ?", Headers.ip_addr, as: {ip: String, count: Int32, date: Int32})
|
||||
rescue ex
|
||||
LOGGER.error "Error when trying to enforce rate limits for ip #{Headers.ip_addr}: #{ex.message}"
|
||||
next
|
||||
ip = Headers.ip_addr
|
||||
if !ip
|
||||
halt env, status_code: 401, response: "X-Real-IP header not present. Contact the admin to fix this!"
|
||||
end
|
||||
|
||||
ip_info = Database::IP.select(ip)
|
||||
|
||||
if ip_info.nil?
|
||||
next
|
||||
end
|
||||
|
||||
time_since_first_upload = Time.utc.to_unix - ip_info[:date]
|
||||
time_until_unban = ip_info[:date] - Time.utc.to_unix + CONFIG.rateLimitPeriod
|
||||
if time_since_first_upload > CONFIG.rateLimitPeriod
|
||||
SQL.exec "DELETE FROM ips WHERE ip = ?", ip_info[:ip]
|
||||
end
|
||||
if CONFIG.filesPerIP > 0
|
||||
if ip_info[:count] >= CONFIG.filesPerIP && time_since_first_upload < CONFIG.rateLimitPeriod
|
||||
if CONFIG.files_per_ip > 0
|
||||
time_since_first_upload = Time.utc.to_unix - ip_info.date
|
||||
time_until_unban = ip_info.date - Time.utc.to_unix + CONFIG.rate_limit_period
|
||||
|
||||
if time_since_first_upload > CONFIG.rate_limit_period
|
||||
Database::IP.delete(ip_info.ip)
|
||||
end
|
||||
|
||||
if ip_info.count >= CONFIG.files_per_ip && time_since_first_upload < CONFIG.rate_limit_period
|
||||
halt env, status_code: 401, response: "Rate limited! Try again in #{time_until_unban} seconds"
|
||||
end
|
||||
end
|
||||
|
@ -69,8 +71,6 @@ module Routing
|
|||
get "/info/chatterino", Routes::Views, :chatterino
|
||||
|
||||
post "/upload", Routes::Upload, :upload
|
||||
# get "/upload", Routes::Upload, :upload_url
|
||||
# post "/api/uploadurl", Routes::Upload, :upload_url
|
||||
|
||||
get "/:filename", Routes::Retrieve, :retrieve_file
|
||||
get "/thumbnail/:thumbnail", Routes::Retrieve, :retrieve_thumbnail
|
||||
|
@ -81,7 +81,7 @@ module Routing
|
|||
get "/info/sharex.sxcu", Routing::Misc, :sharex_config
|
||||
get "/info/chatterinoconfig", Routing::Misc, :chatterino_config
|
||||
|
||||
# if CONFIG.adminEnabled
|
||||
# if CONFIG.admin_enabled
|
||||
# self.register_admin
|
||||
# end
|
||||
end
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
struct IP
|
||||
# Without this, this class will not be able to be used as `as: UFile` on
|
||||
struct UIP
|
||||
# Without this, this class will not be able to be used as `as: IP` on
|
||||
# SQL queries
|
||||
include DB::Serializable
|
||||
|
||||
property ip : String
|
||||
property count : Int32
|
||||
property unix_date : Int32
|
||||
property date : Int64
|
||||
|
||||
def initialize(
|
||||
@ip,
|
||||
@count,
|
||||
@unix_date,
|
||||
@ip = "",
|
||||
@count = 1,
|
||||
@date = 0,
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -3,21 +3,21 @@ struct UFile
|
|||
# SQL queries
|
||||
include DB::Serializable
|
||||
|
||||
property original_filename : String = ""
|
||||
property filename : String = ""
|
||||
property extension : String = ""
|
||||
property uploaded_at : String = ""
|
||||
property checksum : String = ""
|
||||
property ip : String = ""
|
||||
property delete_key : String = ""
|
||||
property original_filename : String
|
||||
property filename : String
|
||||
property extension : String
|
||||
property uploaded_at : Int64
|
||||
property checksum : String?
|
||||
property ip : String
|
||||
property delete_key : String
|
||||
property thumbnail : String?
|
||||
|
||||
def initialize(
|
||||
@original_filename = "",
|
||||
@filename = "",
|
||||
@extension = "",
|
||||
@uploaded_at = "",
|
||||
@checksum = "",
|
||||
@uploaded_at = 0,
|
||||
@checksum = nil,
|
||||
@ip = "",
|
||||
@delete_key = "",
|
||||
@thumbnail = nil,
|
||||
|
|
|
@ -5,7 +5,7 @@ module Utils::Hashing
|
|||
Digest::SHA1.hexdigest &.file(file_path)
|
||||
end
|
||||
|
||||
def hash_io(file_path : IO) : String
|
||||
Digest::SHA1.hexdigest &.update(file_path)
|
||||
def hash_io(file : IO) : String
|
||||
Digest::SHA1.hexdigest &.update(file)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ module Utils::Tor
|
|||
LOGGER.debug "retrieve_tor_exit_nodes: Retrieving Tor exit nodes list"
|
||||
ips = [] of String
|
||||
|
||||
HTTP::Client.get(CONFIG.torExitNodesUrl) do |res|
|
||||
HTTP::Client.get(CONFIG.tor_exit_nodes_url) do |res|
|
||||
begin
|
||||
if res.success? && res.status_code == 200
|
||||
res.body_io.each_line do |line|
|
||||
|
@ -25,7 +25,7 @@ module Utils::Tor
|
|||
LOGGER.error "retrieve_tor_exit_nodes: Failed to retrieve exit nodes list. Status Code: #{res.status_code}"
|
||||
end
|
||||
rescue ex : Socket::ConnectError
|
||||
LOGGER.error "retrieve_tor_exit_nodes: Failed to connect to #{CONFIG.torExitNodesUrl}: #{ex.message}"
|
||||
LOGGER.error "retrieve_tor_exit_nodes: Failed to connect to #{CONFIG.tor_exit_nodes_url}: #{ex.message}"
|
||||
rescue ex
|
||||
LOGGER.error "retrieve_tor_exit_nodes: Unknown error: #{ex.message}"
|
||||
end
|
||||
|
|
|
@ -2,14 +2,70 @@ module Utils
|
|||
extend self
|
||||
|
||||
def create_db
|
||||
if !SQL.query_one "SELECT EXISTS (SELECT 1 FROM sqlite_schema WHERE type='table' AND name='files')
|
||||
AND EXISTS (SELECT 1 FROM sqlite_schema WHERE type='table' AND name='ips');", as: Bool
|
||||
LOGGER.info "Creating sqlite3 database at '#{CONFIG.db}'"
|
||||
files_table = <<-SQL
|
||||
CREATE TABLE
|
||||
IF NOT EXISTS files
|
||||
(
|
||||
original_filename text not null,
|
||||
filename text not null,
|
||||
extension text not null,
|
||||
uploaded_at integer not null,
|
||||
checksum text,
|
||||
ip text not null,
|
||||
delete_key text not null,
|
||||
thumbnail text,
|
||||
PRIMARY KEY(filename)
|
||||
)
|
||||
SQL
|
||||
|
||||
ip_table = <<-SQL
|
||||
CREATE TABLE
|
||||
IF NOT EXISTS ips
|
||||
(
|
||||
ip text,
|
||||
count integer DEFAULT 0,
|
||||
date integer,
|
||||
PRIMARY KEY(ip)
|
||||
)
|
||||
SQL
|
||||
|
||||
files_table_check = <<-SQL
|
||||
SELECT EXISTS
|
||||
(
|
||||
SELECT 1 FROM
|
||||
sqlite_schema
|
||||
WHERE type='table'
|
||||
AND name='files'
|
||||
)
|
||||
SQL
|
||||
|
||||
ip_table_check = <<-SQL
|
||||
SELECT EXISTS
|
||||
(
|
||||
SELECT 1 FROM
|
||||
sqlite_schema
|
||||
WHERE type='table'
|
||||
AND name='ips'
|
||||
)
|
||||
SQL
|
||||
|
||||
files_table_exists = SQL.query_one(files_table_check, as: Bool)
|
||||
ip_table_exists = SQL.query_one(ip_table_check, as: Bool)
|
||||
|
||||
if (!files_table_exists)
|
||||
LOGGER.info "create_db: Creating table 'files'"
|
||||
begin
|
||||
SQL.exec "CREATE TABLE IF NOT EXISTS files
|
||||
(original_filename text, filename text, extension text, uploaded_at text, checksum text, ip text, delete_key text, thumbnail text)"
|
||||
SQL.exec "CREATE TABLE IF NOT EXISTS ips
|
||||
(ip text UNIQUE, count integer DEFAULT 0, date integer)"
|
||||
SQL.exec(files_table)
|
||||
rescue ex
|
||||
LOGGER.fatal "#{ex.message}"
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
if (!ip_table_exists)
|
||||
LOGGER.info "create_db: Creating table 'ips'"
|
||||
begin
|
||||
SQL.exec(ip_table)
|
||||
rescue ex
|
||||
LOGGER.fatal "#{ex.message}"
|
||||
exit(1)
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<body>
|
||||
<div class="container">
|
||||
<h1 style="font-size: 68px; text-align: center; margin: 20px;"><%= host %></h1>
|
||||
<p style="text-align: center; font-size: 22px;"><%= CONFIG.siteInfo %></p>
|
||||
<p style="text-align: center; font-size: 22px;"><%= CONFIG.site_info %></p>
|
||||
<div id="drop-area">
|
||||
<p style='padding: 0;margin: 0; color: #123718bf;'>Arrastra, Pega o Selecciona archivos.</p>
|
||||
<input type="file" id="fileElem" accept="*/*" style="display: none;">
|
||||
|
|
Loading…
Add table
Reference in a new issue