Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd0a0f0c26 | ||
|
|
ee4883774e | ||
|
|
c2ffe57e21 |
@@ -2,4 +2,3 @@ gcc -o 000_parse.exe examples/000_parse.c url.c -I. -Wall -Wextr
|
||||
gcc -o 001_serialize.exe examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb
|
||||
gcc -o 002_partial_parse.exe examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb
|
||||
gcc -o 003_references.exe examples/003_references.c url.c -I. -Wall -Wextra -ggdb
|
||||
gcc -o 004_build.exe examples/004_build.c url.c -I. -Wall -Wextra -ggdb
|
||||
|
||||
@@ -2,4 +2,3 @@ gcc -o 000_parse.out examples/000_parse.c url.c -I. -Wall -Wextr
|
||||
gcc -o 001_serialize.out examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb
|
||||
gcc -o 002_partial_parse.out examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb
|
||||
gcc -o 003_references.out examples/003_references.c url.c -I. -Wall -Wextra -ggdb
|
||||
gcc -o 004_build.out examples/004_build.c url.c -I. -Wall -Wextra -ggdb
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
#include <url.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void)
|
||||
{
|
||||
URL_Builder builder;
|
||||
url_builder_init(&builder);
|
||||
|
||||
url_builder_set_scheme(&builder, "http", -1);
|
||||
|
||||
url_builder_set_username(&builder, "cozis", -1);
|
||||
|
||||
url_builder_set_password(&builder, "my_secret", -1);
|
||||
|
||||
url_builder_set_host_name(&builder, "www.ex@mple.com", -1);
|
||||
|
||||
url_builder_set_path(&builder, "/../../index.html", -1);
|
||||
|
||||
char url[1<<9];
|
||||
int len = url_builder_finalize(&builder, url, sizeof(url));
|
||||
if (len < 0) {
|
||||
printf("Couldn't build URL\n");
|
||||
return -1;
|
||||
}
|
||||
if (len >= (int) sizeof(url)) {
|
||||
printf("URL buffer is too small\n");
|
||||
return -1;
|
||||
}
|
||||
url[len] = '\0';
|
||||
|
||||
printf("Built URL: %s\n", url);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
# Fuzzing the URL Parser
|
||||
|
||||
This directory contains fuzzing infrastructure for the URL parser using AFL++ (American Fuzzy Lop).
|
||||
|
||||
## What is Fuzzing?
|
||||
|
||||
Fuzzing is an automated testing technique that feeds malformed, unexpected, or random data to a program to find bugs, crashes, and security vulnerabilities. AFL++ is a coverage-guided fuzzer that intelligently mutates inputs to maximize code coverage.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Install AFL++
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install afl++
|
||||
```
|
||||
|
||||
**From source:**
|
||||
```bash
|
||||
git clone https://github.com/AFLplusplus/AFLplusplus
|
||||
cd AFLplusplus
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Build the fuzz target:**
|
||||
```bash
|
||||
cd tests/
|
||||
./build_afl.sh
|
||||
```
|
||||
|
||||
2. **Run fuzzing:**
|
||||
```bash
|
||||
./run_afl.sh
|
||||
```
|
||||
|
||||
Or run for a specific duration:
|
||||
```bash
|
||||
./run_afl.sh 60 # Run for 60 minutes
|
||||
```
|
||||
|
||||
3. **View results:**
|
||||
- Crashes: `findings/default/crashes/`
|
||||
- Hangs: `findings/default/hangs/`
|
||||
- Interesting inputs: `findings/default/queue/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── fuzz_afl.c # AFL++ fuzz target
|
||||
├── build_afl.sh # Build script
|
||||
├── run_afl.sh # Run script
|
||||
├── corpus/ # Seed inputs
|
||||
│ ├── basic.txt
|
||||
│ ├── complex.txt
|
||||
│ ├── ipv4.txt
|
||||
│ ├── ipv6.txt
|
||||
│ └── ...
|
||||
└── findings/ # AFL++ output (created when running)
|
||||
└── default/
|
||||
├── crashes/ # Inputs that caused crashes
|
||||
├── hangs/ # Inputs that caused timeouts
|
||||
└── queue/ # All interesting inputs found
|
||||
```
|
||||
|
||||
## What Gets Tested
|
||||
|
||||
The fuzz target tests multiple parser functions:
|
||||
|
||||
- `url_parse()` - Main URL parser (with different flag combinations)
|
||||
- `url_parse_ipv4()` - IPv4 address parser
|
||||
- `url_parse_ipv6()` - IPv6 address parser
|
||||
- `url_serialize()` - URL serialization
|
||||
- `url_percent_decode()` - Percent-decoding
|
||||
- `url_remove_white_space()` - Whitespace removal
|
||||
|
||||
## Analyzing Results
|
||||
|
||||
### Reproducing Crashes
|
||||
|
||||
If AFL++ finds a crash, you can reproduce it:
|
||||
|
||||
```bash
|
||||
./fuzz_afl < findings/default/crashes/id:000000,*
|
||||
```
|
||||
|
||||
Or use a debugger:
|
||||
|
||||
```bash
|
||||
gdb ./fuzz_afl
|
||||
(gdb) run < findings/default/crashes/id:000000,*
|
||||
```
|
||||
|
||||
### Minimizing Crash Inputs
|
||||
|
||||
AFL++ can minimize crash inputs to their smallest form:
|
||||
|
||||
```bash
|
||||
afl-tmin -i findings/default/crashes/id:000000,* -o minimized.txt -- ./fuzz_afl
|
||||
```
|
||||
|
||||
### Viewing Statistics
|
||||
|
||||
While fuzzing is running, AFL++ displays real-time statistics:
|
||||
|
||||
- **Cycles done**: Number of times the fuzzer went through the queue
|
||||
- **Corpus count**: Number of interesting test cases found
|
||||
- **Crashes**: Number of unique crashes found
|
||||
- **Exec speed**: Fuzzing throughput (executions per second)
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Parallel Fuzzing
|
||||
|
||||
Run multiple AFL++ instances in parallel for better coverage:
|
||||
|
||||
```bash
|
||||
# Terminal 1 (master)
|
||||
afl-fuzz -i corpus/ -o findings/ -M fuzzer1 -- ./fuzz_afl
|
||||
|
||||
# Terminal 2 (secondary)
|
||||
afl-fuzz -i corpus/ -o findings/ -S fuzzer2 -- ./fuzz_afl
|
||||
|
||||
# Terminal 3 (secondary)
|
||||
afl-fuzz -i corpus/ -o findings/ -S fuzzer3 -- ./fuzz_afl
|
||||
```
|
||||
|
||||
### Using Different Modes
|
||||
|
||||
AFL++ supports various mutation modes:
|
||||
|
||||
```bash
|
||||
# Explore mode (default)
|
||||
afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl
|
||||
|
||||
# Exploitation mode (focus on found crashes)
|
||||
afl-fuzz -i corpus/ -o findings/ -p exploit -- ./fuzz_afl
|
||||
|
||||
# COE (Continue on Error) mode
|
||||
afl-fuzz -i corpus/ -o findings/ -c -- ./fuzz_afl
|
||||
```
|
||||
|
||||
### With AddressSanitizer (ASAN)
|
||||
|
||||
For better bug detection, rebuild with AddressSanitizer:
|
||||
|
||||
```bash
|
||||
AFL_USE_ASAN=1 ./build_afl.sh
|
||||
./run_afl.sh
|
||||
```
|
||||
|
||||
Note: ASAN significantly slows down fuzzing but can detect memory issues that wouldn't cause crashes.
|
||||
|
||||
### With UndefinedBehaviorSanitizer (UBSAN)
|
||||
|
||||
To detect undefined behavior:
|
||||
|
||||
```bash
|
||||
AFL_USE_UBSAN=1 ./build_afl.sh
|
||||
./run_afl.sh
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Disable CPU frequency scaling:**
|
||||
```bash
|
||||
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
|
||||
```
|
||||
|
||||
2. **Increase system limits:**
|
||||
```bash
|
||||
sudo sysctl -w kernel.core_pattern=core
|
||||
sudo sysctl -w kernel.sched_child_runs_first=1
|
||||
```
|
||||
|
||||
3. **Use persistent mode** (requires code changes to fuzz target)
|
||||
|
||||
4. **Use dictionary files** for better mutations:
|
||||
```bash
|
||||
afl-fuzz -i corpus/ -o findings/ -x url.dict -- ./fuzz_afl
|
||||
```
|
||||
|
||||
## Continuous Fuzzing
|
||||
|
||||
For long-term fuzzing campaigns:
|
||||
|
||||
```bash
|
||||
# Run in screen/tmux
|
||||
screen -S fuzzing
|
||||
./run_afl.sh
|
||||
|
||||
# Detach: Ctrl+A, D
|
||||
# Reattach: screen -r fuzzing
|
||||
```
|
||||
|
||||
Or use a systemd service for continuous fuzzing.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No instrumentation detected"
|
||||
|
||||
Make sure you built with an AFL++ compiler (afl-gcc, afl-clang-fast, etc.)
|
||||
|
||||
### "Suboptimal CPU scaling governor"
|
||||
|
||||
Run as suggested by AFL++ or set `AFL_SKIP_CPUFREQ=1`
|
||||
|
||||
### Low execution speed
|
||||
|
||||
- Close other programs
|
||||
- Use afl-clang-fast instead of afl-gcc
|
||||
- Consider using QEMU mode for uninstrumented binaries
|
||||
|
||||
### No new paths found
|
||||
|
||||
- Add more diverse seed inputs to corpus/
|
||||
- Try different mutation strategies
|
||||
- Consider parallel fuzzing
|
||||
|
||||
## References
|
||||
|
||||
- [AFL++ Documentation](https://github.com/AFLplusplus/AFLplusplus/tree/stable/docs)
|
||||
- [Fuzzing Tutorial](https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md)
|
||||
- [OWASP Fuzzing Guide](https://owasp.org/www-community/Fuzzing)
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# Build script for AFL++ fuzzing
|
||||
#
|
||||
# Prerequisites:
|
||||
# - AFL++ installed (https://github.com/AFLplusplus/AFLplusplus)
|
||||
#
|
||||
# Install AFL++ on Ubuntu/Debian:
|
||||
# sudo apt-get install afl++
|
||||
#
|
||||
# Or build from source:
|
||||
# git clone https://github.com/AFLplusplus/AFLplusplus
|
||||
# cd AFLplusplus
|
||||
# make
|
||||
# sudo make install
|
||||
|
||||
set -e
|
||||
|
||||
echo "Building fuzz target with AFL++..."
|
||||
|
||||
# Try different AFL++ compilers in order of preference
|
||||
if command -v afl-clang-fast &> /dev/null; then
|
||||
echo "Using afl-clang-fast (recommended)"
|
||||
AFL_CC=afl-clang-fast
|
||||
elif command -v afl-gcc &> /dev/null; then
|
||||
echo "Using afl-gcc"
|
||||
AFL_CC=afl-gcc
|
||||
elif command -v afl-clang &> /dev/null; then
|
||||
echo "Using afl-clang"
|
||||
AFL_CC=afl-clang
|
||||
else
|
||||
echo "Error: No AFL++ compiler found!"
|
||||
echo "Please install AFL++ first:"
|
||||
echo " sudo apt-get install afl++"
|
||||
echo "Or see: https://github.com/AFLplusplus/AFLplusplus"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build the fuzz target
|
||||
$AFL_CC -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb
|
||||
|
||||
echo "Build successful! Binary: fuzz_afl"
|
||||
echo ""
|
||||
echo "To run fuzzing:"
|
||||
echo " ./run_afl.sh"
|
||||
@@ -0,0 +1 @@
|
||||
gcc -o fuzz.exe fuzz.c ../url.c -Wall -Wextra -ggdb -O2
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
gcc -o fuzz.out fuzz.c ../url.c -Wall -Wextra -ggdb -O2
|
||||
@@ -0,0 +1 @@
|
||||
//example.com/path
|
||||
@@ -0,0 +1 @@
|
||||
http://example.com
|
||||
@@ -0,0 +1 @@
|
||||
https://user:pass@example.com:8080/path?query=value#fragment
|
||||
@@ -0,0 +1 @@
|
||||
http://example.com/path%20with%20spaces
|
||||
@@ -0,0 +1 @@
|
||||
file:///path/to/file
|
||||
@@ -0,0 +1 @@
|
||||
http://192.168.1.1/path
|
||||
@@ -0,0 +1 @@
|
||||
http://[2001:db8::1]/
|
||||
@@ -0,0 +1 @@
|
||||
http://localhost:3000/api/v1
|
||||
@@ -0,0 +1 @@
|
||||
mailto:user@example.com
|
||||
@@ -0,0 +1 @@
|
||||
../relative/path
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "../url.h"
|
||||
|
||||
// PRNG state
|
||||
static uint64_t rng_state = 0;
|
||||
|
||||
// Simple xorshift64 RNG
|
||||
static uint64_t rand64(void)
|
||||
{
|
||||
rng_state ^= rng_state << 13;
|
||||
rng_state ^= rng_state >> 7;
|
||||
rng_state ^= rng_state << 17;
|
||||
return rng_state;
|
||||
}
|
||||
|
||||
// Generate random byte
|
||||
static uint8_t rand_byte(void)
|
||||
{
|
||||
return (uint8_t)(rand64() & 0xFF);
|
||||
}
|
||||
|
||||
// Generate random integer in range [0, max)
|
||||
static int rand_range(int max)
|
||||
{
|
||||
if (max <= 0) return 0;
|
||||
return (int)(rand64() % max);
|
||||
}
|
||||
|
||||
// Seed URLs for mutation-based fuzzing
|
||||
static const char *seed_urls[] = {
|
||||
"http://example.com",
|
||||
"https://user:pass@example.com:8080/path?query=value#fragment",
|
||||
"ftp://192.168.1.1/file.txt",
|
||||
"http://[2001:db8::1]/",
|
||||
"mailto:user@example.com",
|
||||
"file:///path/to/file",
|
||||
"http://example.com:80/path",
|
||||
"https://example.com/path%20with%20spaces",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:8080/api/v1/resource",
|
||||
"//example.com/path",
|
||||
"/path/to/resource",
|
||||
"../relative/path",
|
||||
"?query=only",
|
||||
"#fragment-only",
|
||||
"http://example.com:99999",
|
||||
"http://256.256.256.256",
|
||||
"http://example..com",
|
||||
"http://:password@example.com",
|
||||
"http://user:@example.com",
|
||||
"http://example.com/path//double//slash",
|
||||
"http://example.com/path/../../../etc/passwd",
|
||||
"http://example.com/%2e%2e%2f",
|
||||
"http://example.com/\x00null",
|
||||
"http://example.com/\x0a\x0d",
|
||||
"http://\t\n\rexample.com",
|
||||
"http://example.com:0",
|
||||
"http://example.com:-1",
|
||||
"http://example.com:abc",
|
||||
"http://[::1]",
|
||||
"http://[::ffff:192.0.2.1]",
|
||||
"http://[2001:db8::1]:8080",
|
||||
"http://[::1",
|
||||
"http://::1]",
|
||||
"data:text/plain;base64,SGVsbG8=",
|
||||
"javascript:alert(1)",
|
||||
"about:blank",
|
||||
};
|
||||
|
||||
#define NUM_SEEDS (sizeof(seed_urls) / sizeof(seed_urls[0]))
|
||||
|
||||
// Character classes for generation
|
||||
static const char *char_classes[] = {
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
"0123456789",
|
||||
"://",
|
||||
"@.:/?#[]",
|
||||
"%",
|
||||
"\t\n\r ",
|
||||
"\x00\x01\x02\x03\x04\x05\x06\x07\x08",
|
||||
};
|
||||
|
||||
#define NUM_CHAR_CLASSES (sizeof(char_classes) / sizeof(char_classes[0]))
|
||||
|
||||
// Mutation strategies
|
||||
typedef enum {
|
||||
MUT_BIT_FLIP,
|
||||
MUT_BYTE_FLIP,
|
||||
MUT_BYTE_INSERT,
|
||||
MUT_BYTE_DELETE,
|
||||
MUT_BYTE_REPLACE,
|
||||
MUT_CHUNK_INSERT,
|
||||
MUT_CHUNK_DELETE,
|
||||
MUT_CHUNK_DUPLICATE,
|
||||
MUT_INTERESTING_VALUE,
|
||||
MUT_COUNT
|
||||
} MutationType;
|
||||
|
||||
// Interesting byte values
|
||||
static const uint8_t interesting_bytes[] = {
|
||||
0x00, 0x01, 0xFF, 0x7F, 0x80,
|
||||
'/', '?', '#', ':', '@', '[', ']',
|
||||
'%', '.', '-', '_', '~',
|
||||
};
|
||||
|
||||
// Mutate a buffer
|
||||
static int mutate_buffer(char *dst, int dst_cap, const char *src, int src_len)
|
||||
{
|
||||
if (src_len <= 0 || src_len >= dst_cap) {
|
||||
// Can't mutate empty or too large buffer
|
||||
return src_len;
|
||||
}
|
||||
|
||||
// Copy source to destination
|
||||
memcpy(dst, src, src_len);
|
||||
int len = src_len;
|
||||
|
||||
// Apply 1-5 random mutations
|
||||
int num_mutations = 1 + rand_range(5);
|
||||
for (int i = 0; i < num_mutations && len > 0 && len < dst_cap - 1; i++) {
|
||||
MutationType mut = rand_range(MUT_COUNT);
|
||||
int pos = rand_range(len);
|
||||
|
||||
switch (mut) {
|
||||
case MUT_BIT_FLIP:
|
||||
if (pos < len) {
|
||||
dst[pos] ^= (1 << rand_range(8));
|
||||
}
|
||||
break;
|
||||
|
||||
case MUT_BYTE_FLIP:
|
||||
if (pos < len) {
|
||||
dst[pos] ^= 0xFF;
|
||||
}
|
||||
break;
|
||||
|
||||
case MUT_BYTE_INSERT:
|
||||
if (len < dst_cap - 1) {
|
||||
memmove(dst + pos + 1, dst + pos, len - pos);
|
||||
dst[pos] = rand_byte();
|
||||
len++;
|
||||
}
|
||||
break;
|
||||
|
||||
case MUT_BYTE_DELETE:
|
||||
if (pos < len && len > 1) {
|
||||
memmove(dst + pos, dst + pos + 1, len - pos - 1);
|
||||
len--;
|
||||
}
|
||||
break;
|
||||
|
||||
case MUT_BYTE_REPLACE:
|
||||
if (pos < len) {
|
||||
dst[pos] = rand_byte();
|
||||
}
|
||||
break;
|
||||
|
||||
case MUT_CHUNK_INSERT: {
|
||||
int chunk_len = 1 + rand_range(16);
|
||||
if (len + chunk_len < dst_cap) {
|
||||
memmove(dst + pos + chunk_len, dst + pos, len - pos);
|
||||
for (int j = 0; j < chunk_len; j++) {
|
||||
dst[pos + j] = rand_byte();
|
||||
}
|
||||
len += chunk_len;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MUT_CHUNK_DELETE: {
|
||||
int chunk_len = 1 + rand_range(16);
|
||||
if (pos + chunk_len <= len) {
|
||||
memmove(dst + pos, dst + pos + chunk_len, len - pos - chunk_len);
|
||||
len -= chunk_len;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MUT_CHUNK_DUPLICATE: {
|
||||
int chunk_len = 1 + rand_range(16);
|
||||
if (pos + chunk_len <= len && len + chunk_len < dst_cap) {
|
||||
memmove(dst + pos + chunk_len, dst + pos, len - pos);
|
||||
memcpy(dst + pos + chunk_len, dst + pos, chunk_len);
|
||||
len += chunk_len;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MUT_INTERESTING_VALUE:
|
||||
if (pos < len) {
|
||||
dst[pos] = interesting_bytes[rand_range(sizeof(interesting_bytes))];
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
// Generate a random URL-like string
|
||||
static int generate_random_url(char *buf, int cap)
|
||||
{
|
||||
int strategy = rand_range(3);
|
||||
|
||||
if (strategy == 0) {
|
||||
// Fully random generation
|
||||
int len = rand_range(cap - 1);
|
||||
for (int i = 0; i < len; i++) {
|
||||
buf[i] = rand_byte();
|
||||
}
|
||||
return len;
|
||||
} else if (strategy == 1) {
|
||||
// Generate from character classes
|
||||
int len = rand_range(cap - 1);
|
||||
for (int i = 0; i < len; i++) {
|
||||
const char *class = char_classes[rand_range(NUM_CHAR_CLASSES)];
|
||||
int class_len = strlen(class);
|
||||
buf[i] = class[rand_range(class_len)];
|
||||
}
|
||||
return len;
|
||||
} else {
|
||||
// Mutation-based: pick a seed and mutate it
|
||||
const char *seed = seed_urls[rand_range(NUM_SEEDS)];
|
||||
int seed_len = strlen(seed);
|
||||
return mutate_buffer(buf, cap, seed, seed_len);
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzz url_parse
|
||||
static void fuzz_url_parse(const char *input, int len)
|
||||
{
|
||||
URL url;
|
||||
int flags_options[] = { 0, URL_FLAG_ALLOWREF, URL_FLAG_RFC3986, URL_FLAG_ALLOWREF | URL_FLAG_RFC3986 };
|
||||
int flags = flags_options[rand_range(4)];
|
||||
|
||||
// Test with NULL cursor
|
||||
url_parse((char *)input, len, NULL, &url, flags);
|
||||
|
||||
// Test with cursor
|
||||
int cursor = 0;
|
||||
url_parse((char *)input, len, &cursor, &url, flags);
|
||||
|
||||
// Test serialization if parse succeeded
|
||||
if (url_parse((char *)input, len, NULL, &url, flags) == 0) {
|
||||
char output[4096];
|
||||
url_serialize(url, NULL, output, sizeof(output));
|
||||
}
|
||||
|
||||
// Test percent decode on random substrings
|
||||
if (len > 0) {
|
||||
int start = rand_range(len);
|
||||
int end = start + rand_range(len - start);
|
||||
URL_String str = { (char *)input + start, end - start };
|
||||
char decoded[4096];
|
||||
url_percent_decode(str, decoded, sizeof(decoded));
|
||||
}
|
||||
|
||||
// Test whitespace removal
|
||||
char tmp[4096];
|
||||
url_remove_white_space((char *)input, len, tmp, sizeof(tmp));
|
||||
}
|
||||
|
||||
// Fuzz url_parse_ipv4
|
||||
static void fuzz_ipv4_parse(const char *input, int len)
|
||||
{
|
||||
URL_IPv4 ipv4;
|
||||
|
||||
// Test with NULL cursor
|
||||
url_parse_ipv4((char *)input, len, NULL, &ipv4);
|
||||
|
||||
// Test with cursor
|
||||
int cursor = 0;
|
||||
url_parse_ipv4((char *)input, len, &cursor, &ipv4);
|
||||
}
|
||||
|
||||
// Fuzz url_parse_ipv6
|
||||
static void fuzz_ipv6_parse(const char *input, int len)
|
||||
{
|
||||
URL_IPv6 ipv6;
|
||||
|
||||
// Test with NULL cursor
|
||||
url_parse_ipv6((char *)input, len, NULL, &ipv6);
|
||||
|
||||
// Test with cursor
|
||||
int cursor = 0;
|
||||
url_parse_ipv6((char *)input, len, &cursor, &ipv6);
|
||||
}
|
||||
|
||||
// Run one fuzz iteration
|
||||
static void fuzz_iteration(int iteration)
|
||||
{
|
||||
char input[4096];
|
||||
int len = generate_random_url(input, sizeof(input));
|
||||
|
||||
// Ensure null termination for safety (though our API doesn't require it)
|
||||
if (len < (int)sizeof(input)) {
|
||||
input[len] = '\0';
|
||||
}
|
||||
|
||||
// Fuzz different functions
|
||||
int target = rand_range(3);
|
||||
switch (target) {
|
||||
case 0:
|
||||
fuzz_url_parse(input, len);
|
||||
break;
|
||||
case 1:
|
||||
fuzz_ipv4_parse(input, len);
|
||||
break;
|
||||
case 2:
|
||||
fuzz_ipv6_parse(input, len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Initialize RNG
|
||||
if (argc > 1) {
|
||||
rng_state = strtoull(argv[1], NULL, 10);
|
||||
} else {
|
||||
rng_state = time(NULL);
|
||||
}
|
||||
|
||||
printf("URL Parser Fuzzer\n");
|
||||
printf("Seed: %llu\n", (unsigned long long)rng_state);
|
||||
printf("Running fuzz tests...\n\n");
|
||||
|
||||
int num_iterations = 100000;
|
||||
if (argc > 2) {
|
||||
num_iterations = atoi(argv[2]);
|
||||
}
|
||||
|
||||
clock_t start = clock();
|
||||
|
||||
for (int i = 0; i < num_iterations; i++) {
|
||||
fuzz_iteration(i);
|
||||
|
||||
// Progress update every 10000 iterations
|
||||
if (i > 0 && i % 10000 == 0) {
|
||||
double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC;
|
||||
double rate = i / elapsed;
|
||||
printf("\rIteration %d/%d (%.0f iter/sec)", i, num_iterations, rate);
|
||||
fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC;
|
||||
printf("\n\nCompleted %d iterations in %.2f seconds\n", num_iterations, elapsed);
|
||||
printf("Average rate: %.0f iterations/second\n", num_iterations / elapsed);
|
||||
printf("\nNo crashes detected!\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* AFL++ fuzz target for url.c parser
|
||||
*
|
||||
* Build with AFL++:
|
||||
* afl-gcc -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb
|
||||
*
|
||||
* Or with afl-clang-fast for better instrumentation:
|
||||
* afl-clang-fast -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb
|
||||
*
|
||||
* Run with:
|
||||
* afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../url.h"
|
||||
|
||||
// Maximum input size
|
||||
#define MAX_INPUT_SIZE 8192
|
||||
|
||||
// Test buffer for outputs
|
||||
static char output_buffer[16384];
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
// Read input from stdin (AFL++ feeds input this way)
|
||||
unsigned char input[MAX_INPUT_SIZE];
|
||||
ssize_t len = read(STDIN_FILENO, input, sizeof(input));
|
||||
|
||||
if (len <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Ensure null termination for safety
|
||||
if (len < (ssize_t)sizeof(input)) {
|
||||
input[len] = '\0';
|
||||
}
|
||||
|
||||
// Fuzz url_parse with different flag combinations
|
||||
URL url;
|
||||
int flags_to_test[] = {
|
||||
0,
|
||||
URL_FLAG_ALLOWREF,
|
||||
URL_FLAG_RFC3986,
|
||||
URL_FLAG_ALLOWREF | URL_FLAG_RFC3986
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int flags = flags_to_test[i];
|
||||
|
||||
// Test with NULL cursor
|
||||
int ret = url_parse((char *)input, len, NULL, &url, flags);
|
||||
|
||||
// Test with cursor
|
||||
int cursor = 0;
|
||||
url_parse((char *)input, len, &cursor, &url, flags);
|
||||
|
||||
// If parse succeeded, test serialization
|
||||
if (ret == 0) {
|
||||
url_serialize(url, NULL, output_buffer, sizeof(output_buffer));
|
||||
|
||||
// Test serialization with itself as base
|
||||
url_serialize(url, &url, output_buffer, sizeof(output_buffer));
|
||||
}
|
||||
}
|
||||
|
||||
// Test whitespace removal
|
||||
int cleaned_len = url_remove_white_space((char *)input, len, output_buffer, sizeof(output_buffer));
|
||||
|
||||
// If whitespace removal succeeded, try parsing the cleaned input
|
||||
if (cleaned_len > 0 && cleaned_len < (int)sizeof(output_buffer)) {
|
||||
url_parse(output_buffer, cleaned_len, NULL, &url, 0);
|
||||
}
|
||||
|
||||
// Test percent decode on the full input
|
||||
URL_String str = { (char *)input, len };
|
||||
url_percent_decode(str, output_buffer, sizeof(output_buffer));
|
||||
|
||||
// Test IPv4 parsing
|
||||
URL_IPv4 ipv4;
|
||||
url_parse_ipv4((char *)input, len, NULL, &ipv4);
|
||||
|
||||
int cursor = 0;
|
||||
url_parse_ipv4((char *)input, len, &cursor, &ipv4);
|
||||
|
||||
// Test IPv6 parsing
|
||||
URL_IPv6 ipv6;
|
||||
url_parse_ipv6((char *)input, len, NULL, &ipv6);
|
||||
|
||||
cursor = 0;
|
||||
url_parse_ipv6((char *)input, len, &cursor, &ipv6);
|
||||
|
||||
// Test parsing with base URL (if input is long enough)
|
||||
if (len > 10) {
|
||||
// Try to use first half as base, second half as reference
|
||||
int mid = len / 2;
|
||||
|
||||
URL base_url;
|
||||
if (url_parse((char *)input, mid, NULL, &base_url, 0) == 0) {
|
||||
URL reference;
|
||||
if (url_parse((char *)input + mid, len - mid, NULL, &reference, URL_FLAG_ALLOWREF) == 0) {
|
||||
url_serialize(reference, &base_url, output_buffer, sizeof(output_buffer));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
# Run AFL++ fuzzing
|
||||
#
|
||||
# Usage:
|
||||
# ./run_afl.sh [duration_in_minutes]
|
||||
#
|
||||
# Examples:
|
||||
# ./run_afl.sh # Run indefinitely (press Ctrl+C to stop)
|
||||
# ./run_afl.sh 60 # Run for 60 minutes
|
||||
|
||||
set -e
|
||||
|
||||
# Check if fuzz target exists
|
||||
if [ ! -f "fuzz_afl" ]; then
|
||||
echo "Error: fuzz_afl binary not found!"
|
||||
echo "Please run ./build_afl.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if AFL++ is installed
|
||||
if ! command -v afl-fuzz &> /dev/null; then
|
||||
echo "Error: afl-fuzz not found!"
|
||||
echo "Please install AFL++:"
|
||||
echo " sudo apt-get install afl++"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
mkdir -p findings
|
||||
|
||||
# Set AFL++ options for better performance
|
||||
export AFL_SKIP_CPUFREQ=1
|
||||
export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1
|
||||
|
||||
echo "Starting AFL++ fuzzing..."
|
||||
echo "Corpus directory: corpus/"
|
||||
echo "Output directory: findings/"
|
||||
echo ""
|
||||
echo "Press Ctrl+C to stop fuzzing"
|
||||
echo ""
|
||||
|
||||
# Run AFL++ with optional timeout and dictionary
|
||||
DICT_ARG=""
|
||||
if [ -f "url.dict" ]; then
|
||||
DICT_ARG="-x url.dict"
|
||||
echo "Using dictionary: url.dict"
|
||||
fi
|
||||
|
||||
if [ -n "$1" ]; then
|
||||
timeout_sec=$((60 * $1))
|
||||
echo "Running for $1 minutes ($timeout_sec seconds)"
|
||||
timeout $timeout_sec afl-fuzz -i corpus/ -o findings/ $DICT_ARG -- ./fuzz_afl || true
|
||||
else
|
||||
afl-fuzz -i corpus/ -o findings/ $DICT_ARG -- ./fuzz_afl
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Fuzzing completed!"
|
||||
echo ""
|
||||
echo "Results:"
|
||||
echo " Crashes: findings/default/crashes/"
|
||||
echo " Hangs: findings/default/hangs/"
|
||||
echo " Queue: findings/default/queue/"
|
||||
echo ""
|
||||
|
||||
# Check for crashes
|
||||
if [ -d "findings/default/crashes" ]; then
|
||||
num_crashes=$(find findings/default/crashes -type f ! -name 'README.txt' | wc -l)
|
||||
if [ $num_crashes -gt 0 ]; then
|
||||
echo "⚠️ Found $num_crashes crash(es)!"
|
||||
echo "To reproduce a crash:"
|
||||
echo " ./fuzz_afl < findings/default/crashes/id:000000,*"
|
||||
else
|
||||
echo "✓ No crashes found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for hangs
|
||||
if [ -d "findings/default/hangs" ]; then
|
||||
num_hangs=$(find findings/default/hangs -type f ! -name 'README.txt' | wc -l)
|
||||
if [ $num_hangs -gt 0 ]; then
|
||||
echo "⚠️ Found $num_hangs hang(s)!"
|
||||
else
|
||||
echo "✓ No hangs found"
|
||||
fi
|
||||
fi
|
||||
+42
-26
@@ -217,73 +217,89 @@ static bool streq(URL_String a, URL_String b)
|
||||
return !memcmp(a.ptr, b.ptr, a.len);
|
||||
}
|
||||
|
||||
static int compare_urls(URL url_1, URL url_2, char *label_1, char *label_2)
|
||||
static int compare_urls(URL parsed, URL expected)
|
||||
{
|
||||
#define UNPACK(s) (s).len, (s).ptr
|
||||
|
||||
bool ok = true;
|
||||
|
||||
if (!streq(url_1.scheme, url_2.scheme)) {
|
||||
if (!streq(parsed.scheme, expected.scheme)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" scheme mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.scheme));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.scheme));
|
||||
test_printf(" parsed [%.*s]\n", UNPACK(parsed.scheme));
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.scheme));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!streq(url_1.username, url_2.username)) {
|
||||
char tmp[1<<9];
|
||||
int ret = url_percent_decode(parsed.username, tmp, sizeof(tmp));
|
||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
||||
|
||||
if (!streq((URL_String) { tmp, ret }, expected.username)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" username mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.username));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.username));
|
||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.username));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!streq(url_1.password, url_2.password)) {
|
||||
ret = url_percent_decode(parsed.password, tmp, sizeof(tmp));
|
||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
||||
|
||||
if (!streq((URL_String) { tmp, ret }, expected.password)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" password mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.password));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.password));
|
||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.password));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!streq(url_1.host_text, url_2.host_text)) {
|
||||
ret = url_percent_decode(parsed.host_text, tmp, sizeof(tmp));
|
||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
||||
|
||||
if (!streq((URL_String) { tmp, ret }, expected.host_text)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" host mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.host_text));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.host_text));
|
||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.host_text));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (url_1.port != url_2.port) {
|
||||
if (parsed.port != expected.port) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" port mismatch\n");
|
||||
test_printf(" %s [%d]\n", label_1, url_1.port);
|
||||
test_printf(" %s [%d]\n", label_2, url_2.port);
|
||||
test_printf(" parsed [%d]\n", parsed.port);
|
||||
test_printf(" expected [%d]\n", expected.port);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!streq(url_1.path, url_2.path)) {
|
||||
if (!streq(parsed.path, expected.path)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" path mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.path));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.path));
|
||||
test_printf(" parsed [%.*s]\n", UNPACK(parsed.path));
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.path));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!streq(url_1.query, url_2.query)) {
|
||||
ret = url_percent_decode(parsed.query, tmp, sizeof(tmp));
|
||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
||||
|
||||
if (!streq((URL_String) { tmp, ret }, expected.query)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" query mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.query));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.query));
|
||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.query));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (!streq(url_1.fragment, url_2.fragment)) {
|
||||
ret = url_percent_decode(parsed.fragment, tmp, sizeof(tmp));
|
||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
||||
|
||||
if (!streq((URL_String) { tmp, ret }, expected.fragment)) {
|
||||
if (ok) test_printf("\n");
|
||||
test_printf(" fragment mismatch\n");
|
||||
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.fragment));
|
||||
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.fragment));
|
||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
||||
test_printf(" expected [%.*s]\n", UNPACK(expected.fragment));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
@@ -394,7 +410,7 @@ static int run_test(cJSON *json)
|
||||
if (json_to_url(json, &expected) < 0)
|
||||
return -1;
|
||||
|
||||
if (!compare_urls(parsed, expected, "parsed", "expected")) {
|
||||
if (!compare_urls(parsed, expected)) {
|
||||
test_printf(" input: ");
|
||||
test_print_str(src);
|
||||
test_printf("\n");
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# AFL++ dictionary for URL fuzzing
|
||||
# This helps AFL++ generate more interesting mutations
|
||||
|
||||
# URL schemes
|
||||
"http://"
|
||||
"https://"
|
||||
"ftp://"
|
||||
"file://"
|
||||
"mailto:"
|
||||
"data:"
|
||||
"ws://"
|
||||
"wss://"
|
||||
|
||||
# Special characters
|
||||
":"
|
||||
"/"
|
||||
"?"
|
||||
"#"
|
||||
"@"
|
||||
"."
|
||||
"%"
|
||||
"["
|
||||
"]"
|
||||
|
||||
# Authority separators
|
||||
"//"
|
||||
"@"
|
||||
|
||||
# Port separators
|
||||
":80"
|
||||
":443"
|
||||
":8080"
|
||||
":3000"
|
||||
|
||||
# Percent encoding
|
||||
"%20"
|
||||
"%2F"
|
||||
"%3A"
|
||||
"%00"
|
||||
"%FF"
|
||||
|
||||
# IPv4 patterns
|
||||
"127.0.0.1"
|
||||
"192.168.1.1"
|
||||
"0.0.0.0"
|
||||
"255.255.255.255"
|
||||
|
||||
# IPv6 patterns
|
||||
"::1"
|
||||
"::"
|
||||
"2001:db8::1"
|
||||
"::ffff:192.0.2.1"
|
||||
|
||||
# Common paths
|
||||
"/index.html"
|
||||
"/api/v1"
|
||||
"/../"
|
||||
"/.."
|
||||
"/."
|
||||
|
||||
# Common query strings
|
||||
"?query=value"
|
||||
"&"
|
||||
"="
|
||||
|
||||
# Common fragments
|
||||
"#fragment"
|
||||
"#top"
|
||||
|
||||
# Userinfo
|
||||
"user:password@"
|
||||
"user@"
|
||||
":password@"
|
||||
|
||||
# Special values
|
||||
"localhost"
|
||||
"example.com"
|
||||
"example.org"
|
||||
|
||||
# Whitespace
|
||||
"\t"
|
||||
"\n"
|
||||
"\r"
|
||||
" "
|
||||
@@ -35,7 +35,7 @@
|
||||
#define UINT16_MAX (URL_u16) ((1 << 16)-1)
|
||||
|
||||
#define NULL ((void*) 0)
|
||||
#define S(X) (URL_String) { (X), sizeof(X)-1 }
|
||||
#define S(X) ((URL_String) { (X), sizeof(X)-1 })
|
||||
#define EMPTY (URL_String) { NULL, 0 }
|
||||
#define SLICE(src, off, end) (URL_String) { src + off, end - off }
|
||||
|
||||
@@ -49,14 +49,6 @@ static void memcpy_(char *dst, char *src, int len)
|
||||
}
|
||||
#endif
|
||||
|
||||
static int strlen_(char *s)
|
||||
{
|
||||
int n = 0;
|
||||
while (s[n] != '\0')
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static URL_b32 streq(URL_String a, URL_String b)
|
||||
{
|
||||
if (a.len != b.len)
|
||||
@@ -532,11 +524,22 @@ int url_parse(char *src, int len, int *pcur, URL *out, int flags)
|
||||
// First, try the IPv4 rule
|
||||
if (cur < len && is_digit(src[cur])) {
|
||||
if (url_parse_ipv4(src, len, &cur, &out->host_ipv4) == 0) {
|
||||
// If we managed to parse an IPv4 address but the
|
||||
// following character si a valid character for a
|
||||
// registered name, then it wasn't an IPv4 after
|
||||
// all. For instance this should not be parsed as
|
||||
// an IPv4:
|
||||
// 127.0.0.1.com
|
||||
if (cur == len || !is_reg_name(src[cur])) {
|
||||
out->host_type = URL_HOST_IPV4;
|
||||
is_ipv4 = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_ipv4)
|
||||
cur = host_off;
|
||||
}
|
||||
|
||||
if (!is_ipv4) {
|
||||
if (cur < len && (is_reg_name(src[cur]) || is_percent_encoded(src, len, cur) == 1)) {
|
||||
|
||||
@@ -792,13 +795,6 @@ static void append(Builder *b, URL_String s)
|
||||
b->len += s.len;
|
||||
}
|
||||
|
||||
static void appendc(Builder *b, char c)
|
||||
{
|
||||
if (b->len < b->cap)
|
||||
b->dst[b->len] = c;
|
||||
b->len++;
|
||||
}
|
||||
|
||||
static void append_port(Builder *b, URL_u16 port)
|
||||
{
|
||||
char buf[sizeof("65536")-1];
|
||||
@@ -1013,162 +1009,3 @@ int url_percent_decode(URL_String str, char *dst, int cap)
|
||||
}
|
||||
return wr;
|
||||
}
|
||||
|
||||
static void append_percent_encoded(Builder *b, URL_String str, URL_b32 (*testfn)(char c))
|
||||
{
|
||||
for (int i = 0; i < str.len; i++) {
|
||||
char c = str.ptr[i];
|
||||
if (testfn(c))
|
||||
appendc(b, str.ptr[i]);
|
||||
else {
|
||||
static const char table[] = "0123456789abcdef";
|
||||
appendc(b, '%');
|
||||
appendc(b, table[(URL_u8) c >> 4]);
|
||||
appendc(b, table[(URL_u8) c & 0xF]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void url_builder_init(URL_Builder *ub)
|
||||
{
|
||||
ub->scheme = EMPTY;
|
||||
ub->username = EMPTY;
|
||||
ub->password = EMPTY;
|
||||
ub->host_type = URL_HOST_EMPTY;
|
||||
ub->no_port = 1;
|
||||
ub->port = 0;
|
||||
ub->path = EMPTY;
|
||||
ub->query = EMPTY;
|
||||
ub->fragment = EMPTY;
|
||||
}
|
||||
|
||||
void url_builder_set_scheme(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->scheme = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
void url_builder_set_username(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->username = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
void url_builder_set_password(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->password = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
void url_builder_set_host_name(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->host_type = URL_HOST_NAME;
|
||||
ub->host_name = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
void url_builder_set_host_ipv4(URL_Builder *ub, URL_IPv4 ipv4)
|
||||
{
|
||||
ub->host_type = URL_HOST_IPV4;
|
||||
ub->host_ipv4 = ipv4;
|
||||
}
|
||||
|
||||
void url_builder_set_host_ipv6(URL_Builder *ub, URL_IPv6 ipv6)
|
||||
{
|
||||
ub->host_type = URL_HOST_IPV6;
|
||||
ub->host_ipv6 = ipv6;
|
||||
}
|
||||
|
||||
void url_builder_set_port(URL_Builder *ub, URL_u16 port)
|
||||
{
|
||||
ub->no_port = 0;
|
||||
ub->port = port;
|
||||
}
|
||||
|
||||
void url_builder_set_path(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->path = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
void url_builder_set_query(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->query = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
void url_builder_set_fragment(URL_Builder *ub, char *str, int len)
|
||||
{
|
||||
if (len < 0) len = str ? strlen_(str) : 0;
|
||||
ub->fragment = (URL_String) { str, len };
|
||||
}
|
||||
|
||||
int url_builder_finalize(URL_Builder *ub, char *dst, int cap)
|
||||
{
|
||||
Builder b = { dst, cap, 0 };
|
||||
|
||||
// Validate and append the schema
|
||||
{
|
||||
if (ub->scheme.len == 0 || !is_scheme_first(ub->scheme.ptr[0]))
|
||||
return -1;
|
||||
for (int i = 1; i < ub->scheme.len; i++)
|
||||
if (!is_scheme(ub->scheme.ptr[i]))
|
||||
return -1;
|
||||
append(&b, ub->scheme);
|
||||
}
|
||||
|
||||
URL_b32 authority = 0;
|
||||
if (ub->username.len > 0 || ub->password.len > 0 || ub->host_type != URL_HOST_EMPTY || ub->no_port == 0) {
|
||||
authority = 1;
|
||||
appendc(&b, '/');
|
||||
appendc(&b, '/');
|
||||
if (ub->username.len > 0 || ub->password.len > 0) {
|
||||
append_percent_encoded(&b, ub->username, is_userinfo);
|
||||
appendc(&b, ':');
|
||||
append_percent_encoded(&b, ub->password, is_userinfo);
|
||||
appendc(&b, '@');
|
||||
}
|
||||
switch (ub->host_type) {
|
||||
case URL_HOST_EMPTY:
|
||||
// Do nothing
|
||||
break;
|
||||
case URL_HOST_NAME:
|
||||
append_percent_encoded(&b, ub->host_name, is_reg_name);
|
||||
break;
|
||||
case URL_HOST_IPV4:
|
||||
ASSERT(0); // TODO
|
||||
break;
|
||||
case URL_HOST_IPV6:
|
||||
appendc(&b, '[');
|
||||
ASSERT(0);
|
||||
appendc(&b, ']');
|
||||
break;
|
||||
}
|
||||
if (ub->no_port == 0) {
|
||||
appendc(&b, ':');
|
||||
append_port(&b, ub->port);
|
||||
}
|
||||
}
|
||||
|
||||
if (ub->path.len > 0) {
|
||||
|
||||
// If an authority is present, the path can't be relative
|
||||
if (authority && ub->path.ptr[0] != '/')
|
||||
return -1;
|
||||
|
||||
PathComps comps = {0};
|
||||
if (resolve_dots_and_append_comps(&comps, ub->path) < 0)
|
||||
return -1;
|
||||
for (int i = 0; i < comps.count; i++) {
|
||||
if (i > 0 || comps.first_slash)
|
||||
append(&b, S("/"));
|
||||
append_percent_encoded(&b, comps.stack[i], is_pchar);
|
||||
}
|
||||
if (comps.trailing_slash)
|
||||
append(&b, S("/"));
|
||||
}
|
||||
|
||||
append_query(&b, ub->query);
|
||||
append_fragment(&b, ub->fragment);
|
||||
return b.len;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ typedef enum {
|
||||
URL_HOST_NAME,
|
||||
} URL_HostType;
|
||||
|
||||
// This struct's fields are strictly read-only.
|
||||
typedef struct {
|
||||
|
||||
// Flags used to parse this URL
|
||||
@@ -175,39 +174,4 @@ int url_percent_decode(URL_String str, char *dst, int cap);
|
||||
// though it's just a best-effor normalization for now.
|
||||
int url_serialize(URL url, URL *base, char *dst, int cap);
|
||||
|
||||
typedef struct {
|
||||
|
||||
URL_String scheme;
|
||||
|
||||
URL_String username;
|
||||
URL_String password;
|
||||
|
||||
URL_HostType host_type;
|
||||
union {
|
||||
URL_String host_name;
|
||||
URL_IPv4 host_ipv4;
|
||||
URL_IPv6 host_ipv6;
|
||||
};
|
||||
|
||||
URL_b32 no_port;
|
||||
URL_u16 port;
|
||||
|
||||
URL_String path;
|
||||
URL_String query;
|
||||
URL_String fragment;
|
||||
} URL_Builder;
|
||||
|
||||
void url_builder_init(URL_Builder *ub);
|
||||
void url_builder_set_scheme(URL_Builder *ub, char *str, int len);
|
||||
void url_builder_set_username(URL_Builder *ub, char *str, int len);
|
||||
void url_builder_set_password(URL_Builder *ub, char *str, int len);
|
||||
void url_builder_set_host_name(URL_Builder *ub, char *str, int len);
|
||||
void url_builder_set_host_ipv4(URL_Builder *ub, URL_IPv4 ipv4);
|
||||
void url_builder_set_host_ipv6(URL_Builder *ub, URL_IPv6 ipv6);
|
||||
void url_builder_set_port(URL_Builder *ub, URL_u16 port);
|
||||
void url_builder_set_path(URL_Builder *ub, char *str, int len);
|
||||
void url_builder_set_query(URL_Builder *ub, char *str, int len);
|
||||
void url_builder_set_fragment(URL_Builder *ub, char *str, int len);
|
||||
int url_builder_finalize(URL_Builder *ub, char *dst, int cap);
|
||||
|
||||
#endif // URL_INCLUDED
|
||||
|
||||
Reference in New Issue
Block a user