Dateien mit Lua und LuaSocket nach Amazon S3 exportieren
LuaSocket bietet Netzwerkprimitive und Wartezeiten im Sekundenbruchteilbereich, implementiert aber
selbst weder AWS-Authentifizierung noch die Validierung von HTTPS-Zertifikaten. Dieses Beispiel
verwendet die FFI von LuaJIT, um den gepflegten SigV4-Transport von libcurl aufzurufen. LuaSocket
übernimmt die Wartezeit zwischen Wiederholungsversuchen. Das Beispiel ersetzt den bisherigen,
ungetesteten Konstruktor von lua-resty-aws und die selbst geschriebene
Signaturimplementierung.
Installation und Einrichtung
Verwenden Sie LuaJIT 2.1, LuaSocket 3.1.0-1 und einen
unterstützten libcurl-Build mit HTTPS und AWS SigV4. Das Beispiel wurde mit libcurl
8.21.0 getestet. Es nutzt Optionen, die seit libcurl
7.85.0 verfügbar sind. Verwenden Sie eine aktuell gepatchte Version statt
dieser historischen Mindestversion. Installieren Sie libcurl und LuaJIT mit dem Paketmanager Ihres
Systems und anschließend LuaSocket für denselben LuaJIT-Interpreter:
luarocks --lua-version=5.1 install luasocket 3.1.0-1
LuaRocks muss für die Header und den Interpreter von LuaJIT konfiguriert sein. Setzen Sie
LIBCURL_PATH auf den vertrauenswürdigen absoluten Pfad Ihrer gemeinsam genutzten
libcurl-Bibliothek, falls diese nicht im üblichen Suchpfad des dynamischen Laders liegt.
Dieses Beispiel ist für macOS und Linux vorgesehen. Es benötigt weder OpenResty noch LuaSec.
AWS-Zugangsdaten einrichten
Verwenden Sie Zugangsdaten mit der Berechtigung, Objekte im vorgesehenen Bucket und unter dem
vorgesehenen Präfix abzulegen. Bevorzugen Sie temporäre Zugangsdaten und geben Sie alle drei
Variablen an: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY und
AWS_SESSION_TOKEN. Bei langlebigen Zugangsdaten entfällt das Sitzungstoken. Beziehen Sie
die Zugangsdaten aus dem Secret-Management Ihrer Bereitstellungsumgebung. Betten Sie
sie nicht in den Quellcode ein.
Setzen Sie AWS_REGION und S3_BUCKET auf die tatsächliche
Region und den Namen des Buckets. Der folgende Standardendpunkt unterstützt herkömmliche Buckets
in kommerziellen AWS-Regionen, deren Namen Kleinbuchstaben, Ziffern und Bindestriche enthalten.
Andere Partitionen, Namen mit Punkten, Zugriffspunkte und Verzeichnis-Buckets benötigen eine eigene,
geprüfte Endpunktkonfiguration. IAM-Berechtigungen und das tatsächliche Verhalten des Buckets
müssen im Live-Betrieb validiert werden.
Einen geprüften Transport zur Signierung verwenden
Die Option CURLOPT_AWS_SIGV4 von libcurl signiert die Anfrage.
Das Sitzungstoken wird als x-amz-security-token gesendet und in die signierten Header
aufgenommen. Schlüssel werden als UTF-8-Bytes codiert, wobei Schrägstriche als Trennzeichen erhalten
bleiben. Das Modul lehnt die eigenständigen Pfadkomponenten . und
.. ab, da der SigV4-Modus von libcurl
PATH_AS_IS nicht unterstützt. Eine stillschweigende Normalisierung dieser
Komponenten würde den Objektschlüssel ändern. Die Signierung berechnet den Hash exakt über die Bytes
von POSTFIELDS, obwohl die HTTP-Methode PUT lautet.
So muss im Tutorial keine separate kryptografische Implementierung gepflegt werden.
Aus LuaJIT hochladen
Speichern Sie dieses Modul als s3_upload.lua. Es begrenzt jedes Objekt im
Arbeitsspeicher bewusst auf 16 MiB. Größere Uploads benötigen eine separat
geprüfte Streaming- oder Multipart-Implementierung. endpoint und
cafile sind optionale, vertrauenswürdige Einstellungen der
Bereitstellungsumgebung, die für Tests mit einer HTTPS-Fixture nützlich sind.
Übernehmen Sie diese niemals aus einer nicht authentifizierten Anfrage.
local ffi = require("ffi")
local socket = require("socket")
ffi.cdef[[
typedef void CURL;
struct curl_slist { char *data; struct curl_slist *next; };
int curl_global_init(long flags);
CURL *curl_easy_init(void);
void curl_easy_cleanup(CURL *handle);
int curl_easy_setopt(CURL *handle, int option, ...);
int curl_easy_perform(CURL *handle);
int curl_easy_getinfo(CURL *handle, int info, ...);
struct curl_slist *curl_slist_append(struct curl_slist *list, const char *value);
void curl_slist_free_all(struct curl_slist *list);
]]
local curl = ffi.load(os.getenv("LIBCURL_PATH") or "curl")
assert(curl.curl_global_init(3) == 0, "Cannot initialize libcurl")
-- Stable option identifiers from libcurl's public curl.h ABI.
local O = { URL=10002, PROXY=10004, WRITEFUNCTION=20011, POSTFIELDS=10015,
HTTPHEADER=10023, CUSTOMREQUEST=10036, SSL_VERIFYPEER=64, CAINFO=10065,
POSTFIELDSIZE=60, SSL_VERIFYHOST=81, CONNECTTIMEOUT=78, TIMEOUT=13,
USERNAME=10173, PASSWORD=10174, AWS_SIGV4=10305,
PROTOCOLS_STR=10318, FOLLOWLOCATION=52 }
local M = {}
function M.encode_key(key)
assert(type(key) == "string" and #key > 0 and #key <= 1024, "Invalid object key")
for part in key:gmatch("[^/]+") do
assert(part ~= "." and part ~= "..", "Dot path components are unsupported")
end
return (key:gsub("([^A-Za-z0-9/_.~-])", function(byte)
return string.format("%%%02X", byte:byte())
end))
end
local function header(value)
assert(type(value) == "string" and #value > 0 and not value:find("[%c]"), "Invalid header value")
return value
end
local function request(config, key, body, content_type)
local handle = curl.curl_easy_init()
assert(handle ~= nil, "Cannot allocate curl handle")
local headers = nil
local function append_header(value)
local updated = curl.curl_slist_append(headers, value)
assert(updated ~= nil, "Cannot allocate curl header")
headers = updated
end
local received = 0
local writer = ffi.cast("size_t (*)(char *, size_t, size_t, void *)", function(_, size, count)
local length = tonumber(size * count)
received = received + length
if received > 1024 * 1024 then return 0 end
return length
end)
local function option(name, value)
if type(value) == "number" then value = ffi.new("long", value) end
assert(curl.curl_easy_setopt(handle, O[name], value) == 0, "Unsupported curl option: " .. name)
end
local ok, result = pcall(function()
local endpoint = config.endpoint or ("https://" .. config.bucket .. ".s3." .. config.region .. ".amazonaws.com")
assert(endpoint:match("^https://[A-Za-z0-9.-]+:?%d*$"), "Expected a trusted HTTPS origin")
option("URL", endpoint .. "/" .. M.encode_key(key))
option("PROTOCOLS_STR", "https")
option("PROXY", "")
option("FOLLOWLOCATION", 0)
option("SSL_VERIFYPEER", 1)
option("SSL_VERIFYHOST", 2)
if config.cafile then option("CAINFO", config.cafile) end
option("CONNECTTIMEOUT", 10)
option("TIMEOUT", 60)
option("USERNAME", header(config.access_key))
option("PASSWORD", header(config.secret_key))
option("AWS_SIGV4", "aws:amz:" .. config.region .. ":s3")
option("CUSTOMREQUEST", "PUT")
option("POSTFIELDSIZE", #body)
option("POSTFIELDS", body)
option("WRITEFUNCTION", writer)
for _, value in ipairs({"Content-Type: " .. header(content_type), "Expect:"}) do
append_header(value)
end
if config.session_token then
append_header("x-amz-security-token: " .. header(config.session_token))
end
option("HTTPHEADER", headers)
local code = curl.curl_easy_perform(handle)
local status = ffi.new("long[1]")
assert(curl.curl_easy_getinfo(handle, 0x200002, status) == 0, "Cannot read HTTP status")
return { transport=tonumber(code), status=tonumber(status[0]) }
end)
curl.curl_easy_cleanup(handle)
curl.curl_slist_free_all(headers)
writer:free()
if not ok then error(result) end
return result
end
-- libcurl calls a Lua callback; this call path must stay outside compiled FFI traces.
jit.off(request, true)
function M.retryable(result)
if result.transport ~= 0 then
return ({[6]=true, [7]=true, [18]=true, [28]=true, [55]=true, [56]=true})[result.transport] == true
end
return ({[408]=true, [429]=true, [500]=true, [502]=true, [503]=true, [504]=true})[result.status] == true
end
function M.upload(config, file_path, key, content_type)
assert(config.region and config.region:match("^[a-z0-9-]+$"), "Invalid region")
assert(config.bucket and #config.bucket >= 3 and #config.bucket <= 63
and config.bucket:match("^[a-z0-9][a-z0-9-]*[a-z0-9]$"), "Invalid bucket name")
local file = assert(io.open(file_path, "rb"))
local body, read_error = file:read(16 * 1024 * 1024 + 1)
file:close()
if read_error then error(read_error) end
body = body or ""
assert(#body <= 16 * 1024 * 1024, "Use multipart upload for larger files")
for attempt = 1, 4 do
local result = request(config, key, body, content_type)
if result.transport == 0 and result.status == 200 then return true end
if attempt == 4 or not M.retryable(result) then
return nil, "Upload failed (transport " .. result.transport .. ", HTTP " .. result.status .. ")"
end
socket.sleep(math.min(8, 0.25 * 2^(attempt - 1)) * (0.5 + math.random()))
end
end
return M
Speichern Sie diesen aufrufenden Code als upload.lua:
local s3 = require("s3_upload")
local ok, err = s3.upload({
access_key = os.getenv("AWS_ACCESS_KEY_ID"),
secret_key = os.getenv("AWS_SECRET_ACCESS_KEY"),
session_token = os.getenv("AWS_SESSION_TOKEN"),
region = os.getenv("AWS_REGION"),
bucket = os.getenv("S3_BUCKET")
}, assert(arg[1], "local file required"), assert(arg[2], "object key required"), "application/octet-stream")
assert(ok, err)
print("Uploaded")
luajit upload.lua report.pdf 'reports/September report.pdf'
Fehlerbehandlung und Wiederholungsversuche
Nur HTTP 200 bestätigt diesen Vorgang mit
PutObject. Weiterleitungen, Authentifizierungsfehler und andere dauerhafte
HTTP-Fehler führen zum sofortigen Abbruch. Bei ausgewählten vorübergehenden Transportfehlern,
Drosselung und Dienstfehlern erfolgen höchstens vier Versuche. Bei Fehlern der Zertifikatsvalidierung
wird kein erneuter Versuch unternommen. Die Wartezeit nutzt socket.sleep() mit
Sekundenbruchteilen. Dabei wird niemals ein Shell-Befehl erstellt oder eine Bruchzahl mit einem
Ganzzahlplatzhalter formatiert.
Jeder Wiederholungsversuch signiert eine neue Anfrage mit denselben Bytes und demselben Schlüssel.
Eine verloren gegangene Antwort kann dennoch dazu führen, dass mehrere Versionen in einem
versionierten Bucket entstehen oder Benachrichtigungen wiederholt werden. Klären Sie mehrdeutige
Ergebnisse, wenn diese Auswirkungen relevant sind. Dieses bewusst begrenzte Beispiel implementiert
weder adaptive Drosselung noch die Verarbeitung von Retry-After, die automatische
Erneuerung von Zugangsdaten oder eine garantiert genau einmalige Auslieferung.
Inhaltstypen behandeln
Der aufrufende Code verwendet application/octet-stream. Geben Sie gegebenenfalls den bekannten
Inhaltstyp an, etwa application/pdf. Eine Dateinamenerweiterung allein validiert den
Dateiinhalt nicht. Das Modul lehnt Steuerzeichen in Header-Werten ab und gibt den Antworttext des
Dienstes niemals als Fehler zurück.
Fazit
Verwenden Sie einen gepflegten Transport zur Signierung und prüfen Sie codierte Schlüssel, temporäre Zugangsdaten, TLS-Fehler und Antworten ohne Erfolgsstatus an einem isolierten HTTPS-Endpunkt. Lokale Protokollprüfungen können weder IAM-Autorisierung noch Bucket-Richtlinien oder die Verfügbarkeit von Objekten auf AWS nachweisen. Führen Sie diese Prüfungen separat mit einem Test-Bucket durch, für den Sie autorisiert sind.
