.
This commit is contained in:
commit
2ed9805b67
9 changed files with 459 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build/
|
||||
12
LICENSE
Normal file
12
LICENSE
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
BSD Zero Clause License
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
||||
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
||||
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
||||
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
||||
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
21
LICENSE-MIT
Normal file
21
LICENSE-MIT
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Brian Khuu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
18
README
Normal file
18
README
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
parses a http body and pipes all its multipart/form-data
|
||||
to a child program. set's the MIME_TYPE environment variable
|
||||
before exec'ing into the child.
|
||||
|
||||
field names are ignored.
|
||||
|
||||
external deps:
|
||||
- libowfat
|
||||
|
||||
This project is licensed under the 0BSD license except for
|
||||
the following third-party files:
|
||||
./minimal_multipart_parser.c
|
||||
./minimal_multipart_parser.h.
|
||||
Copyright (c) 2024 Brian Khuu
|
||||
MIT License
|
||||
https://briankhuu.com/
|
||||
|
||||
https://github.com/mofosyne/minimal-multipart-form-data-parser-c?tab=MIT-1-ov-file
|
||||
23
htpipe.1
Normal file
23
htpipe.1
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
.TH htpipe 1
|
||||
.SH NAME
|
||||
htpipe \- pipe HTTP header fields
|
||||
.SH SYNOPSIS
|
||||
htpipe PROG ARGS
|
||||
.SH DESCRIPTION
|
||||
htpipe is meant to run under a superserver such
|
||||
as tcpserver(1) or inetd(8).
|
||||
It reads an HTTP request from stdin and pipes the content
|
||||
of the first HTTP multipart/form-data field to PROG.
|
||||
Specifically it exec's into PROG where the first
|
||||
argument is set to PROG and the remaining to ARGS.
|
||||
.PP
|
||||
If the HTTP request specifies no multipart/form-data field,
|
||||
htpipe responds with "400 Bad Request" and exits with 100.
|
||||
.PP
|
||||
On any other internal error, it responds with "500 Internal
|
||||
Server Error" and exits with 111.
|
||||
.SH "EXAMPLE"
|
||||
An echo HTTP service:
|
||||
tcpserver 0 80 htpipe nobotgate-access -n cat
|
||||
.SH "SEE ALSO"
|
||||
nobotgate-access(1), tcpserver(1), inetd(8)
|
||||
81
htpipe.c
Normal file
81
htpipe.c
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#include "minimal_multipart_parser.h"
|
||||
#include <llhttp.h>
|
||||
#include <libowfat/buffer.h>
|
||||
#include <libowfat/errmsg.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#undef die
|
||||
#undef diesys
|
||||
#define die(n,...) \
|
||||
do { \
|
||||
carp(__VA_ARGS__,(char*)0); \
|
||||
dieservererr(n,__VA_ARGS__,(char*)0); \
|
||||
} while(0)
|
||||
#define diesys(n,...) \
|
||||
do { \
|
||||
carpsys(__VA_ARGS__,(char*)0); \
|
||||
dieservererr(n,__VA_ARGS__,(char*)0); \
|
||||
} while(0)
|
||||
|
||||
static buffer topipe;
|
||||
|
||||
static void dieservererr(int n,char *args,...)
|
||||
{
|
||||
char *header = "HTTP/1.0 500 Internal Server Error\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
msg(header,args);
|
||||
exit(n);
|
||||
}
|
||||
|
||||
static void dienodata(void)
|
||||
{
|
||||
char *header = "HTTP/1.0 400 Bad Request\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
msg(header,"HTTP multipart/form-data field expected.");
|
||||
exit(100);
|
||||
}
|
||||
|
||||
static void run(void) {
|
||||
MinimalMultipartParserContext ctx = {0};
|
||||
unsigned int ev;
|
||||
int r;
|
||||
char c;
|
||||
|
||||
for (;;) {
|
||||
r = buffer_getc(buffer_0,&c);
|
||||
if (r==0) break;
|
||||
if (r<0) diesys(111,"read stdin");
|
||||
|
||||
ev = minimal_multipart_parser_process(&ctx,c);
|
||||
if (ev==MultipartParserEvent_DataBufferAvailable) {
|
||||
if (buffer_put(&topipe,
|
||||
minimal_multipart_parser_get_data_buffer(&ctx),
|
||||
minimal_multipart_parser_get_data_size(&ctx))<0
|
||||
) diesys(111,"write to pipe");
|
||||
} else if (ev==MultipartParserEvent_DataStreamCompleted)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!minimal_multipart_parser_is_file_received(&ctx))
|
||||
dienodata();
|
||||
if (buffer_flush(&topipe)<0)
|
||||
diesys(111,"flush pipe write");
|
||||
}
|
||||
|
||||
int main(int argc,char **argv)
|
||||
{
|
||||
char buf[4096];
|
||||
int pipefds[2];
|
||||
argc--; argv++;
|
||||
(void)argc;
|
||||
|
||||
if (pipe(pipefds)<0) diesys(111,"pipe");
|
||||
buffer_init_write(&topipe,pipefds[1],buf,sizeof buf);
|
||||
|
||||
run();
|
||||
if (dup2(pipefds[0],0)<0) diesys(111,"dup2");
|
||||
execvp(*argv,argv);
|
||||
diesys(111,"exec ",*argv);
|
||||
}
|
||||
17
meson.build
Normal file
17
meson.build
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
project(
|
||||
'htpipe', 'c',
|
||||
default_options: [
|
||||
'c_std=c99',
|
||||
'warning_level=2',
|
||||
],
|
||||
)
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
deps = [
|
||||
cc.find_library('libowfat'),
|
||||
]
|
||||
|
||||
executable('htpipe', files(
|
||||
'./htpipe.c',
|
||||
'./minimal_multipart_parser.c',
|
||||
), dependencies:deps, install:true)
|
||||
211
minimal_multipart_parser.c
Normal file
211
minimal_multipart_parser.c
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
//
|
||||
// minimal_multipart_parser.c
|
||||
//
|
||||
// Copyright (c) 2024 Brian Khuu https://briankhuu.com/
|
||||
// MIT licensed
|
||||
//
|
||||
// https://github.com/mofosyne/minimal-multipart-form-data-parser-c
|
||||
//
|
||||
|
||||
#include "minimal_multipart_parser.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
static inline unsigned int buffer_count(MinimalMultipartParserCharBuffer *context) { return context->count; }
|
||||
|
||||
static inline void buffer_reset(MinimalMultipartParserCharBuffer *context)
|
||||
{
|
||||
context->buffer[0] = '\0';
|
||||
context->count = 0;
|
||||
}
|
||||
|
||||
static inline bool buffer_add(MinimalMultipartParserCharBuffer *context, const char c)
|
||||
{
|
||||
if (context->count >= MINIMAL_MULTIPART_PARSER_FULL_BOUNDARY_BUFFER_MAX_CHAR)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context->buffer[context->count] = c;
|
||||
context->buffer[context->count + 1] = '\0';
|
||||
context->count++;
|
||||
return true;
|
||||
}
|
||||
|
||||
MultipartParserEvent minimal_multipart_parser_process(MinimalMultipartParserContext *context, const char c)
|
||||
{
|
||||
MinimalMultipartParserCharBuffer *boundaryBuffer = &(context->boundary);
|
||||
MinimalMultipartParserCharBuffer *dataBuffer = &(context->data);
|
||||
|
||||
if (context->data_available)
|
||||
{
|
||||
buffer_reset(dataBuffer);
|
||||
context->data_available = false;
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_INIT)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\r':
|
||||
context->phase = MultipartParserPhase_Preamble_CR;
|
||||
return MultipartParserEvent_None;
|
||||
case '-':
|
||||
context->phase = MultipartParserPhase_Preamble_HYPHEN;
|
||||
return MultipartParserEvent_None;
|
||||
default:
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_Preamble_SKIP_LINE)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\r':
|
||||
context->phase = MultipartParserPhase_Preamble_CR;
|
||||
return MultipartParserEvent_None;
|
||||
case '\n':
|
||||
context->phase = MultipartParserPhase_Preamble_LF;
|
||||
return MultipartParserEvent_None;
|
||||
default:
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_Preamble_CR)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\n':
|
||||
context->phase = MultipartParserPhase_Preamble_LF;
|
||||
return MultipartParserEvent_None;
|
||||
default:
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_Preamble_LF)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '-':
|
||||
context->phase = MultipartParserPhase_Preamble_HYPHEN;
|
||||
return MultipartParserEvent_None;
|
||||
default:
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_Preamble_HYPHEN)
|
||||
{
|
||||
// Previously got a dash in '\r\n--', seeking another dash
|
||||
switch (c)
|
||||
{
|
||||
case '-':
|
||||
context->phase = MultipartParserPhase_GetBoundary;
|
||||
buffer_add(boundaryBuffer, '\r');
|
||||
buffer_add(boundaryBuffer, '\n');
|
||||
buffer_add(boundaryBuffer, '-');
|
||||
buffer_add(boundaryBuffer, '-');
|
||||
return MultipartParserEvent_None;
|
||||
default:
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_GetBoundary)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\r':
|
||||
context->phase = MultipartParserPhase_GetBoundary_Done;
|
||||
return MultipartParserEvent_None;
|
||||
default:
|
||||
if ((c < ' ') || ('~' < c))
|
||||
{
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
buffer_reset(dataBuffer);
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
if (!buffer_add(boundaryBuffer, c))
|
||||
{
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
buffer_reset(dataBuffer);
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_GetBoundary_Done)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '\n':
|
||||
context->phase = MultipartParserPhase_SkipFileHeader;
|
||||
return MultipartParserEvent_FileStreamFound;
|
||||
default:
|
||||
context->phase = MultipartParserPhase_Preamble_SKIP_LINE;
|
||||
buffer_reset(dataBuffer);
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_SkipFileHeader)
|
||||
{
|
||||
const unsigned expected_index = buffer_count(dataBuffer);
|
||||
const char expected_char = (MINIMAL_MULTIPART_PARSER_FILE_START_MARKER)[expected_index];
|
||||
if (c != expected_char)
|
||||
{
|
||||
buffer_reset(dataBuffer);
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
|
||||
buffer_add(dataBuffer, c);
|
||||
if (buffer_count(dataBuffer) >= MINIMAL_MULTIPART_PARSER_FILE_START_MARKER_COUNT)
|
||||
{
|
||||
context->phase = MultipartParserPhase_GetFileBytes;
|
||||
buffer_reset(dataBuffer);
|
||||
return MultipartParserEvent_FileStreamStarting;
|
||||
}
|
||||
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_GetFileBytes)
|
||||
{
|
||||
const unsigned char full_boundary_size = buffer_count(boundaryBuffer);
|
||||
const char *full_boundary_string = context->boundary.buffer;
|
||||
|
||||
const unsigned expected_index = buffer_count(dataBuffer);
|
||||
const char expected_char = full_boundary_string[expected_index];
|
||||
|
||||
buffer_add(dataBuffer, c);
|
||||
|
||||
if (c != expected_char)
|
||||
{
|
||||
context->data_available = true;
|
||||
return MultipartParserEvent_DataBufferAvailable;
|
||||
}
|
||||
|
||||
if (buffer_count(dataBuffer) >= full_boundary_size)
|
||||
{
|
||||
context->phase = MultipartParserPhase_EndOfFile;
|
||||
return MultipartParserEvent_DataStreamCompleted;
|
||||
}
|
||||
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
|
||||
if (context->phase == MultipartParserPhase_EndOfFile)
|
||||
{
|
||||
// Do nothing... We already got the file now
|
||||
return MultipartParserEvent_DataStreamCompleted;
|
||||
}
|
||||
|
||||
return MultipartParserEvent_None;
|
||||
}
|
||||
75
minimal_multipart_parser.h
Normal file
75
minimal_multipart_parser.h
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//
|
||||
// minimal_multipart_parser.h
|
||||
//
|
||||
// Copyright (c) 2024 Brian Khuu https://briankhuu.com/
|
||||
// MIT licensed
|
||||
//
|
||||
// https://github.com/mofosyne/minimal-multipart-form-data-parser-c
|
||||
//
|
||||
|
||||
#ifndef MINIMAL_MULTIPART_PARSER_H
|
||||
#define MINIMAL_MULTIPART_PARSER_H
|
||||
|
||||
#include "minimal_multipart_parser.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
// Size of the full boundary string we are searching for as a multipart file divider
|
||||
// e.g. `\r\n--BOUNDARY` where BOUNDARY is a user specified 70 bytes long printable ascii string
|
||||
#define MINIMAL_MULTIPART_PARSER_USER_BOUNDARY_SIZE (70)
|
||||
#define MINIMAL_MULTIPART_PARSER_FULL_BOUNDARY_BUFFER_MAX_CHAR (4 + MINIMAL_MULTIPART_PARSER_USER_BOUNDARY_SIZE)
|
||||
|
||||
#define MINIMAL_MULTIPART_PARSER_BOUNDARY_START_MARKER "\r\n--"
|
||||
#define MINIMAL_MULTIPART_PARSER_BOUNDARY_START_MARKER_COUNT (sizeof(MINIMAL_MULTIPART_PARSER_BOUNDARY_START_MARKER) - 1)
|
||||
|
||||
#define MINIMAL_MULTIPART_PARSER_FILE_START_MARKER "\r\n\r\n"
|
||||
#define MINIMAL_MULTIPART_PARSER_FILE_START_MARKER_COUNT (sizeof(MINIMAL_MULTIPART_PARSER_FILE_START_MARKER) - 1)
|
||||
|
||||
#define MINIMAL_MULTIPART_PARSER_MAX_CHAR (MINIMAL_MULTIPART_PARSER_FULL_BOUNDARY_BUFFER_MAX_CHAR)
|
||||
|
||||
typedef enum MultipartParserEvent
|
||||
{
|
||||
MultipartParserEvent_None,
|
||||
MultipartParserEvent_FileStreamFound,
|
||||
MultipartParserEvent_FileStreamStarting,
|
||||
MultipartParserEvent_DataBufferAvailable,
|
||||
MultipartParserEvent_DataStreamCompleted
|
||||
} MultipartParserEvent;
|
||||
|
||||
typedef enum MultipartParserPhase
|
||||
{
|
||||
MultipartParserPhase_INIT,
|
||||
MultipartParserPhase_Preamble_SKIP_LINE,
|
||||
MultipartParserPhase_Preamble_CR,
|
||||
MultipartParserPhase_Preamble_LF,
|
||||
MultipartParserPhase_Preamble_HYPHEN,
|
||||
MultipartParserPhase_GetBoundary,
|
||||
MultipartParserPhase_GetBoundary_Done,
|
||||
MultipartParserPhase_SkipFileHeader,
|
||||
MultipartParserPhase_GetFileBytes,
|
||||
MultipartParserPhase_EndOfFile
|
||||
} MultipartParserPhase;
|
||||
|
||||
typedef struct MinimalMultipartParserCharBuffer
|
||||
{
|
||||
char buffer[MINIMAL_MULTIPART_PARSER_MAX_CHAR + 1];
|
||||
unsigned int count;
|
||||
} MinimalMultipartParserCharBuffer;
|
||||
|
||||
typedef struct MinimalMultipartParserContext
|
||||
{
|
||||
MultipartParserPhase phase;
|
||||
MinimalMultipartParserCharBuffer boundary;
|
||||
MinimalMultipartParserCharBuffer data;
|
||||
|
||||
bool data_available;
|
||||
} MinimalMultipartParserContext;
|
||||
|
||||
static inline const unsigned int minimal_multipart_parser_get_data_size(const MinimalMultipartParserContext *context) { return context->data.count; }
|
||||
|
||||
static inline const char *minimal_multipart_parser_get_data_buffer(const MinimalMultipartParserContext *context) { return context->data.buffer; }
|
||||
|
||||
static inline const bool minimal_multipart_parser_is_file_received(const MinimalMultipartParserContext *context) { return context->phase == MultipartParserPhase_EndOfFile; }
|
||||
|
||||
MultipartParserEvent minimal_multipart_parser_process(MinimalMultipartParserContext *context, const char c);
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue