This commit is contained in:
kento2 2026-09-09 04:16:40 +02:00
commit 2ed9805b67
9 changed files with 459 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build/

12
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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