Added Additional features

Postgres database and diesel was added
History capability has been added
Added additional handlebars pages to view and submit get/post requests via form.
This commit is contained in:
Camerin Figueroa 2024-03-04 22:19:39 +00:00
commit e02426797d
17 changed files with 3048 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL=postgres://username:password@localhost/request_mirror_db

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
.env.local

24
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Release",
"program": "${workspaceFolder}/target/release/request-mirror",
"args": ["--release"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug",
"program": "${workspaceFolder}/target/debug/request-mirror",
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

1939
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

27
Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "request-mirror"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "0.4.34"
diesel = { version = "2.1.4", features = ["postgres", "chrono"] }
dotenvy = "0.15.7"
rocket = "0.4.10"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
tokio = { version = "1.36.0", features = ["full"] }
[dependencies.rocket_contrib]
version = "0.4.10"
features = ["handlebars_templates", "tera_templates"]
[dependencies.uuid]
version = "1.7.0"
features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
]

9
diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId"]
[migrations_directory]
dir = "migrations"

0
migrations/.keep Normal file
View File

View File

@ -0,0 +1,5 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS clients;
DROP TABLE IF EXISTS ownership;
DROP TABLE IF EXISTS history;
DROP TABLE IF EXISTS pair_records;

View File

@ -0,0 +1,31 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS clients
(
id SERIAL PRIMARY KEY,
ip TEXT NOT NULL,
mirror_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS ownership
(
id SERIAL PRIMARY KEY,
owner_id TEXT NOT NULL,
client_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS history
(
id SERIAL PRIMARY KEY,
client_id TEXT NOT NULL,
request_type TEXT NOT NULL,
timestamp TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS pair_records
(
id SERIAL PRIMARY KEY,
history_id INTEGER NOT NULL,
pair_type INTEGER NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL
);

53
src/lib.rs Normal file
View File

@ -0,0 +1,53 @@
pub mod models;
pub mod schema;
use chrono::offset::Utc;
use diesel::prelude::*;
use dotenvy::dotenv;
use models::{
Client, NewClient, NewHistoryRecord, NewPairRecord
};
use schema::{
clients, history, pair_records
};
use std::env;
pub fn establish_connection() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
pub fn create_client(conn: &mut PgConnection, ip: &str, mirror_id: &str) -> usize {
let new_client = NewClient {ip: &ip, mirror_id: &mirror_id};
diesel::insert_into(clients::table)
.values(&new_client)
.returning(Client::as_returning())
.execute(conn)
.expect("Error")
}
pub fn create_history_record(conn: &mut PgConnection, client_id: &str, request_type: &str, ) -> i32 {
let new_history_record = NewHistoryRecord {
client_id: &client_id,
request_type: request_type,
timestamp: Utc::now().naive_utc()
};
diesel::insert_into(history::table)
.values(&new_history_record)
.returning(history::columns::id)
//.execute(conn).unwrap()
.get_result(conn).unwrap()
}
pub fn create_pair_record(conn: &mut PgConnection, history_id: i32, pair_type: i32, key: &str, value: &str) -> usize {
let new_pair_record = NewPairRecord {history_id: history_id, pair_type: pair_type, key: &key, value: &value};
diesel::insert_into(pair_records::table)
.values(&new_pair_record)
.execute(conn).unwrap()
}

393
src/main.rs Normal file
View File

@ -0,0 +1,393 @@
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use]
extern crate rocket;
use request_mirror::schema::{history, pair_records};
use rocket::{data, request, Outcome, Request, Data};
use rocket::data::FromDataSimple;
use rocket::request::FromRequest;
use rocket_contrib::templates::Template;
use serde::Serialize;
use rocket::http::{Cookie, Cookies, Status};
use uuid::Uuid;
use std::io::Read;
use request_mirror::models::*;
use diesel::prelude::*;
use request_mirror::*;
#[derive(Serialize, Debug, Clone)]
enum PairType {
Header,
Cookie,
Query,
Body
}
#[derive(Serialize, Debug, Clone)]
struct Pair {
key: String,
value: String
}
#[derive(Serialize, Debug, Clone)]
struct RequestInfo {
header: Vec<Pair>,
cookies: Vec<Pair>,
query: Vec<Pair>
}
#[derive(Serialize, Debug, Clone)]
struct RequestBody(String);
#[derive(Debug)]
enum ApiError {
}
#[derive(Serialize)]
struct ErrorContext {
error_msg: String
}
// Always use a limit to prevent DoS attacks.
const LIMIT: u64 = 256;
impl<'a, 'r> FromRequest<'a, 'r> for RequestInfo {
type Error = ApiError;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let mut req_cookies = request.cookies();
for row in req_cookies.iter() {
println!("{}: {}", row.name(), row.value());
}
// Initially set cookie
if req_cookies.get(&"mirror-id").is_none() {
let new_uuid = Uuid::new_v4().to_string();
println!("Creating new cookie");
req_cookies.add(Cookie::new("mirror-id", new_uuid.clone()));
let address = if request.client_ip().is_some() {
request.client_ip().unwrap().to_string()
} else {
"Unknown".to_string()
};
let connection = &mut establish_connection();
create_client(connection, &address, &new_uuid);
}
let mut header: Vec<Pair> = vec![];
let mut cookies: Vec<Pair> = vec![];
let mut query: Vec<Pair> = vec![];
// Compile header
for row in request.headers().clone().into_iter() {
let key: String = row.name().to_string();
let value: String = row.value().to_string();
header.push(Pair{key:key, value:value});
}
// Compile cookies
for row in req_cookies.iter() {
let key: String = row.name().to_string();
let value: String = row.value().to_string();
cookies.push(Pair{key:key, value:value});
}
// Compile query
let request_query = request.raw_query_items();
if request_query.is_some() {
for row in request_query.unwrap() {
let (key, value) = row.key_value_decoded();
query.push(Pair{key:key, value:value});
}
}
Outcome::Success(RequestInfo{
header: header,
cookies: cookies,
query: query
})
}
}
impl FromDataSimple for RequestBody {
type Error = String;
fn from_data(_req: &Request, data: Data) -> data::Outcome<Self, String> {
// Read the data into a String.
let mut string = String::new();
if let Err(e) = data.open().take(LIMIT).read_to_string(&mut string) {
return Outcome::Failure((Status::InternalServerError, format!("{:?}", e)));
}
// Return successfully.
Outcome::Success(RequestBody(string))
}
}
#[get("/")]
fn index(_info: RequestInfo) -> Template {
//Redirect::to("/test")
#[derive(Serialize)]
struct Context{
}
Template::render("index", Context {})
}
#[get("/test")]
fn test_get(request: RequestInfo, cookies: Cookies) -> Template {
println!("{request:?}");
let cookie_id = cookies.get("mirror-id");
if cookie_id.is_some() {
let connection = &mut establish_connection();
let history_id = create_history_record(connection, cookie_id.unwrap().value(), "Get");
println!("Creating header Records for {history_id}");
for row in &request.header {
create_pair_record(connection, history_id, PairType::Header as i32, &row.key, &row.value);
}
println!("Creating cookie Records for {history_id}");
for row in &request.cookies {
create_pair_record(connection, history_id, PairType::Cookie as i32, &row.key, &row.value);
}
println!("Creating query Records for {history_id}");
for row in &request.query {
create_pair_record(connection, history_id, PairType::Query as i32, &row.key, &row.value);
}
}
#[derive(Serialize)]
struct Context<'a> {
request_type: &'a str,
header: Vec<Pair>,
cookies: Vec<Pair>,
query: Vec<Pair>,
}
let context = Context {
request_type: "Get",
header: request.header,
cookies: request.cookies,
query: request.query
};
Template::render("request_details", context)
}
#[post("/test", data = "<body>")]
fn test_post(body: RequestBody, request: RequestInfo, cookies: Cookies) -> Template {
println!("{request:?}");
println!("Input: {}", body.0);
let cookie_id = cookies.get("mirror-id");
if cookie_id.is_some() {
let connection = &mut establish_connection();
let history_id = create_history_record(connection, cookie_id.unwrap().value(), "Post");
println!("Creating header Records for {history_id}");
for row in &request.header {
create_pair_record(connection, history_id, PairType::Header as i32, &row.key, &row.value);
}
println!("Creating cookie Records for {history_id}");
for row in &request.cookies {
create_pair_record(connection, history_id, PairType::Cookie as i32, &row.key, &row.value);
}
println!("Creating query Records for {history_id}");
for row in &request.query {
create_pair_record(connection, history_id, PairType::Query as i32, &row.key, &row.value);
}
println!("Creating body Records for {history_id}");
create_pair_record(connection, history_id, PairType::Body as i32, "body", &body.0.clone());
}
#[derive(Serialize)]
struct Context<'a> {
request_type: &'a str,
header: Vec<Pair>,
cookies: Vec<Pair>,
query: Vec<Pair>,
body: String
}
let context = Context {
request_type: "Post",
header: request.header,
cookies: request.cookies,
query: request.query,
body: body.0
};
Template::render("request_details", context)
}
#[get("/history")]
fn history_req(cookies: Cookies) -> Template {
let cookie_id = cookies.get("mirror-id");
let cookie_id = cookie_id.unwrap().value();
println!("Client ID: {}", cookie_id);
let connection = &mut establish_connection();
let results = history::dsl::history
.filter(history::client_id.eq(cookie_id.to_string()))
.select(HistoryRecord::as_select())
.load(connection)
.expect("Error loading clients");
for record in results.iter() {
println!("{:?}", record);
}
#[derive(Serialize)]
struct History {
pub id: i32,
pub client_id: String,
pub request_type: String,
pub timestamp: String
}
#[derive(Serialize)]
struct Context {
history_records: Vec<History>
}
let mut history_records: Vec<History> = Vec::new();
for history_rec in results {
history_records.push(
History {
id: history_rec.id,
client_id: history_rec.client_id,
request_type: history_rec.request_type,
timestamp: history_rec.timestamp.format("%Y-%m-%d %H:%M:%S UTC").to_string()
}
);
}
Template::render("history", Context{
history_records: history_records
})
}
#[get("/history/<history_id>")]
fn history_details(history_id: i32, cookies: Cookies) -> Template {
let cookie_id = cookies.get("mirror-id");
let cookie_id = cookie_id.unwrap().value();
println!("Client ID: {}", cookie_id);
let connection = &mut establish_connection();
let results: Vec<HistoryRecord> = history::dsl::history
.filter(history::id.eq(&history_id))
.filter(history::client_id.eq(cookie_id.to_string()))
.select(HistoryRecord::as_select())
.load(connection)
.expect("Error loading history records");
if results.len() <= 0 {
// Error
return Template::render(
"error",
ErrorContext{ error_msg: "No Results Found. You may be unauthorized...".to_string() }
);
}
let connection = &mut establish_connection();
let pairs: Vec<PairRecord> = pair_records::dsl::pair_records
.filter(pair_records::history_id.eq(history_id))
.select(PairRecord::as_select())
.load(connection)
.expect("Error loading history records");
let body: String = match &pairs.iter().filter(|res| res.pair_type == PairType::Body as i32).last() {
Some(pair) => pair.value.clone(),
_ => "".to_string()
};
let header: Vec<&PairRecord> = pairs.iter()
.filter(|res: &&PairRecord| res.pair_type == PairType::Header as i32)
.map(|res: &PairRecord| res)
.collect();
let cookies: Vec<&PairRecord> = pairs.iter()
.filter(|res: &&PairRecord| res.pair_type == PairType::Cookie as i32)
.map(|res: &PairRecord| res)
.collect();
let query: Vec<&PairRecord> = pairs.iter()
.filter(|res: &&PairRecord| res.pair_type == PairType::Query as i32)
.map(|res: &PairRecord| res)
.collect();
#[derive(Serialize)]
struct Context<'a> {
request_type: String,
body: String,
header: Vec<&'a PairRecord>,
cookies: Vec<&'a PairRecord>,
query: Vec<&'a PairRecord>,
}
Template::render(
"request_details",
Context{
request_type: results[0].request_type.clone(),
body: body.to_string(),
header: header,
cookies: cookies,
query: query
})
}
#[catch(404)]
fn not_found(req: &Request) -> String {
print!("{}", req);
format!("Oh no! We couldn't find the requested path '{}'", req.uri())
}
fn main() {
rocket::ignite()
.register(catchers![not_found])
.mount("/", routes![index, test_get, test_post, history_req, history_details])
.attach(Template::fairing())
.launch();
}

63
src/models.rs Normal file
View File

@ -0,0 +1,63 @@
use chrono::NaiveDateTime;
use diesel::prelude::*;
use serde::Serialize;
use crate::schema::{
clients,
history,
pair_records
};
use diesel::pg::Pg;
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = clients)]
#[diesel(check_for_backend(Pg))]
pub struct Client {
pub id: i32,
pub ip: String,
pub mirror_id: String,
}
#[derive(Insertable)]
#[diesel(table_name = clients)]
pub struct NewClient<'a> {
pub ip: &'a str,
pub mirror_id: &'a str,
}
#[derive(Queryable, Selectable, Debug)]
#[diesel(table_name = history)]
#[diesel(check_for_backend(Pg))]
pub struct HistoryRecord {
pub id: i32,
pub client_id: String,
pub request_type: String,
pub timestamp: NaiveDateTime
}
#[derive(Insertable)]
#[diesel(table_name = history)]
pub struct NewHistoryRecord<'a> {
pub client_id: &'a str,
pub request_type: &'a str,
pub timestamp: NaiveDateTime
}
#[derive(Queryable, Selectable, Debug, Serialize)]
#[diesel(table_name = pair_records)]
#[diesel(check_for_backend(Pg))]
pub struct PairRecord {
pub id: i32,
pub history_id: i32,
pub pair_type: i32,
pub key: String,
pub value: String
}
#[derive(Insertable)]
#[diesel(table_name = pair_records)]
pub struct NewPairRecord<'a> {
pub history_id: i32,
pub pair_type: i32,
pub key: &'a str,
pub value: &'a str
}

43
src/schema.rs Normal file
View File

@ -0,0 +1,43 @@
// @generated automatically by Diesel CLI.
diesel::table! {
clients (id) {
id -> Int4,
ip -> Text,
mirror_id -> Text,
}
}
diesel::table! {
history (id) {
id -> Int4,
client_id -> Text,
request_type -> Text,
timestamp -> Timestamp,
}
}
diesel::table! {
ownership (id) {
id -> Int4,
owner_id -> Text,
client_id -> Text,
}
}
diesel::table! {
pair_records (id) {
id -> Int4,
history_id -> Int4,
pair_type -> Int4,
key -> Text,
value -> Text,
}
}
diesel::allow_tables_to_appear_in_same_query!(
clients,
history,
ownership,
pair_records,
);

50
templates/error.hbs Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body class="d-flex flex-column">
<nav id="navbar" class="navbar navbar-expand-lg bg-body-tertiary mb-2">
<div class="container-fluid">
<a class="navbar-brand" href="/">Request Mirror</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/test">Test Page</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/history">History</a>
</li>
</ul>
</div>
</div>
</nav>
<div id="body" class="p-5">
<h1>Error Page</h1>
<p>{{error_msg}}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>

83
templates/history.hbs Normal file
View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body class="d-flex flex-column">
<nav id="navbar" class="navbar navbar-expand-lg bg-body-tertiary mb-5">
<div class="container-fluid">
<a class="navbar-brand" href="/">Request Mirror</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/test">Test Page</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/history">History</a>
</li>
</ul>
</div>
</div>
</nav>
<div id="body" class="p-5">
<div class="tab-pane fade show active" id="header" role="tabpanel" aria-labelledby="header-tab">
<h1>Request History</h1>
<table class="table table-striped">
<tr>
<th>History ID</th>
<th>Client ID</th>
<th>Request Type</th>
<th>TimeStamp</th>
</tr>
{{#each history_records}}
<tr>
<td><a href="/history/{{id}}">{{id}}</a></td>
<td>{{client_id}}</td>
<td>{{request_type}}</td>
<td>{{timestamp}}</td>
</tr>
{{/each}}
</table>
</div>
</div>
<script type="text/javascript">
let records = [
{{#each history_records}}
{
id: {{id}},
client_id:"{{client_id}}",
request_type: "{{request_type}}",
timestamp:"{{timestamp}}"
},
{{/each}}
];
function popupRecord(event, id) {
console.log(records.filter(v=>v.id == id));
}
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>

193
templates/index.hbs Normal file
View File

@ -0,0 +1,193 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body class="d-flex flex-column">
<nav id="navbar" class="navbar navbar-expand-lg bg-body-tertiary mb-5">
<div class="container-fluid">
<a class="navbar-brand" href="/">Request Mirror</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/test">Test Page</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/history">History</a>
</li>
</ul>
</div>
</div>
</nav>
<div id="body" class="p-5">
<h1>Test Form</h1>
<div class="form-check form-switch">
<input onchange="onRequestMethod(event)" class="form-check-input" type="checkbox" role="switch"
id="requestMethodSwitch">
<label class="form-check-label" for="requestMethodSwitch" id="requestMethodLabel">Post Request</label>
</div>
<div id="function-header">
<button class="btn btn-secondary" type="button" onclick="addField()">Add Field</button>
<button class="btn btn-secondary" type="button" onclick="saveFields()">Save Form</button>
<button class="btn btn-secondary" type="button" onclick="loadFields()">Load Form</button>
<button class="btn btn-secondary" type="button" onclick="resetForm()">Reset Form</button>
</div>
<form id="submission-form" method="post" action="/test" onsubmit="saveForm()">
<table class="m-2" style="width: 100rem;">
<tbody id="field-groups">
<tr id="table-header">
<th>Value</th>
<th>Name</th>
<th>Type</th>
</tr>
</tbody>
</table>
<input class="btn btn-primary" type="submit" value="Submit"/>
</form>
</div>
<script type="text/javascript">
let fieldCount = 0;
let fieldInfo = {
}
function saveFields() {
localStorage.setItem('mirror-save', JSON.stringify(fieldInfo));
}
function loadFields() {
let fieldstr = localStorage.getItem('mirror-save');
if (fieldstr) {
fieldInfo = JSON.parse(fieldstr);
resetForm();
for (let field in fieldInfo) {
console.log(field);
addField(fieldInfo[field]);
}
}
}
function resetForm() {
localStorage.removeItem('mirror-save');
fieldInfo = {};
for (let i = 0; i < fieldCount; i++) {
document.getElementById(`row-${i}`).remove();;
}
fieldCount = 0;
}
function addField(field) {
console.log(field);
let newInput = document.createElement('tr');
newInput.id = `row-${fieldCount}`;
newInput.innerHTML = `
<td>
<input class="form-control" id='${fieldCount}-input' type='text' name='${fieldCount}' onchange="onValueChange('${fieldCount}', event)" value='${field?field.value:""}'/>
</td>
<td >
<input class="form-control" id="${fieldCount}-name" type='text' onchange="onNameChange('${fieldCount}', event);" value='${field?field.name:fieldCount}'>
</td>
<td>
<select class="form-select col-auto" id="${fieldCount}-type" onchange='onInputTypeSelect("${fieldCount}", event)' aria-label="Field Type">
<option value='text' selected>Text</option>
<option value='email'>Email</option>
<option value='password'>Password</option>
<option value='number'>Number</option>
<option value='range'>Range</option>
<option value='url'>Url</option>
<option value='date'>Date</option>
<option value='datetime-local'>Date Time</option>
<option value='color'>Color</option>
<option value='checkbox'>Checkbox</option>
<option value='radio'>Radio</option>
<option value='file'>File</option>
</select>
</td>
<td>
<button type="button" class='btn btn-danger' onclick="delField(${fieldCount})">-</button>
</td>`;
for (let element in document.getElementById(`${fieldCount}-type`)) {
if(element.value === field.type) {
element.selected = true;
}
}
fieldInfo[fieldCount.toString()] = {
name: fieldCount.toString(),
value: "",
type: "text"
}
document.getElementById("field-groups").appendChild(newInput);
fieldCount++;
}
function delField(id) {
delete fieldInfo[id];
document.getElementById("field-groups").removeChild(document.getElementById(id))
}
function onValueChange(id, event) {
fieldInfo[id].value = event.target.value;
}
function onNameChange(id, event) {
fieldInfo[id].name = event.target.value;
document.getElementById(`${id}-input`).name = event.target.value;
}
function onInputTypeSelect(id, event) {
fieldInfo[id].type = event.target.value;
document.getElementById(`${id}-input`).type = event.target.value;
}
function onRequestMethod(event) {
document.getElementById('requestMethodLabel').innerText = event.target.checked ? 'Get Request' : 'Post Request';
document.getElementById('submission-form').method = event.target.checked ? 'get' : 'post';
}
</script>
<script type="text/javascript" defer>
loadFields();
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body class="d-flex flex-column">
<nav id="navbar" class="navbar navbar-expand-lg bg-body-tertiary mb-5">
<div class="container-fluid">
<a class="navbar-brand" href="/">Request Mirror</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/test">Test Page</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/history">History</a>
</li>
</ul>
</div>
</div>
</nav>
<div id="body" class="d-flex flex-column p-4 container-xxl">
<h1>{{request_type}} Request</h1>
{{#if body}}
<div class="mb-2 mt-2 input-group">
<span class="input-group-text">Request Body</span>
<textarea class="form-control" aria-label="With textarea" readonly>{{body}}</textarea>
</div>
{{/if}}
<!-- Tab Header-->
<ul class="nav nav-tabs" id="RequestTabs" role="tablist">
{{#if header}}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="header-tab" data-bs-toggle="tab" data-bs-target="#header" type="button"
role="tab" aria-controls="header" aria-selected="true">Header</button>
</li>
{{/if}}
{{#if cookies}}
<li class="nav-item" role="presentation">
<button class="nav-link" id="cookies-tab" data-bs-toggle="tab" data-bs-target="#cookies" type="button"
role="tab" aria-controls="cookies" aria-selected="false">Cookies</button>
</li>
{{/if}}
{{#if query}}
<li class="nav-item" role="presentation">
<button class="nav-link" id="query-tab" data-bs-toggle="tab" data-bs-target="#query" type="button" role="tab"
aria-controls="query" aria-selected="false">Url Query Params</button>
</li>
{{/if}}
</ul>
<div class="tab-content mr-auto ml-auto" id="myTabContent">
{{#if header}}
<div class="tab-pane fade show active" id="header" role="tabpanel" aria-labelledby="header-tab">
<table class="table table-striped">
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{#each header}}
<tr>
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{{/each}}
</table>
</div>
{{/if}}
{{#if cookies}}
<div class="tab-pane fade" id="cookies" role="tabpanel" aria-labelledby="cookies-tab">
<table class="table table-striped">
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{#each cookies}}
<tr>
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{{/each}}
</table>
</div>
{{/if}}
{{#if query}}
<div class="tab-pane fade" id="query" role="tabpanel" aria-labelledby="query-tab">
<table class="table table-striped">
<tr>
<th>Key</th>
<th>Value</th>
</tr>
{{#each query}}
<tr>
<td>{{key}}</td>
<td>{{value}}</td>
</tr>
{{/each}}
</table>
</div>
{{/if}}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>
</html>