use std::collections::HashMap;
use std::convert::TryFrom;
use std::ffi::OsString;
use std::fs;
use std::io::{self, Write};
use std::path::PathBuf;
use anyhow::{anyhow, Context, Result};
use reqwest::header::HeaderMap;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use crate::auth;
use crate::utils::{config_dir, test_mode};
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
enum Meta {
Xh { about: String, xh: String },
Httpie { httpie: String },
Other,
}
impl Default for Meta {
fn default() -> Self {
Meta::Xh {
about: "xh session file".into(),
xh: xh_version(),
}
}
}
#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Auth {
#[serde(rename = "type")]
auth_type: Option<String>,
raw_auth: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cookie {
value: String,
#[serde(skip_serializing_if = "Option::is_none")]
expires: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
secure: Option<bool>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Content {
#[serde(rename = "__meta__")]
meta: Meta,
auth: Auth,
cookies: HashMap<String, Cookie>,
headers: HashMap<String, String>,
}
impl Content {
fn migrate(mut self) -> Self {
self.meta = Meta::default();
self
}
}
pub struct Session {
pub path: PathBuf,
pub read_only: bool,
content: Content,
}
impl Session {
pub fn load_session(url: &Url, mut name_or_path: OsString, read_only: bool) -> Result<Self> {
let path = if is_path(&name_or_path) {
PathBuf::from(name_or_path)
} else {
let mut path = config_dir()
.context("couldn't get config directory")?
.join::<PathBuf>(["sessions", &path_from_url(url)?].iter().collect());
name_or_path.push(".json");
path.push(name_or_path);
path
};
let content = match fs::read_to_string(&path) {
Ok(content) => serde_json::from_str::<Content>(&content)?.migrate(),
Err(err) if err.kind() == io::ErrorKind::NotFound => Content::default(),
Err(err) => return Err(err.into()),
};
Ok(Session {
path,
read_only,
content,
})
}
pub fn headers(&self) -> Result<HeaderMap> {
Ok(HeaderMap::try_from(&self.content.headers)?)
}
pub fn save_headers(&mut self, request_headers: &HeaderMap) -> Result<()> {
for (key, value) in request_headers.iter() {
let key = key.as_str();
if key != "cookie" && !key.starts_with("content-") && !key.starts_with("if-") {
self.content
.headers
.insert(key.into(), value.to_str()?.into());
}
}
Ok(())
}
pub fn auth(&self) -> Result<Option<auth::Auth>> {
if let Auth {
auth_type: Some(auth_type),
raw_auth: Some(raw_auth),
} = &self.content.auth
{
match auth_type.as_str() {
"basic" => {
let (username, password) = auth::parse_auth(raw_auth, "")?;
Ok(Some(auth::Auth::Basic(username, password)))
}
"digest" => {
let (username, password) = auth::parse_auth(raw_auth, "")?;
Ok(Some(auth::Auth::Digest(
username,
password.unwrap_or_else(|| "".into()),
)))
}
"bearer" => Ok(Some(auth::Auth::Bearer(raw_auth.into()))),
_ => Err(anyhow!("Unknown auth type {}", raw_auth)),
}
} else {
Ok(None)
}
}
pub fn save_auth(&mut self, auth: &auth::Auth) {
match auth {
auth::Auth::Basic(username, password) => {
let password = password.as_deref().unwrap_or("");
self.content.auth = Auth {
auth_type: Some("basic".into()),
raw_auth: Some(format!("{}:{}", username, password)),
}
}
auth::Auth::Digest(username, password) => {
self.content.auth = Auth {
auth_type: Some("digest".into()),
raw_auth: Some(format!("{}:{}", username, password)),
}
}
auth::Auth::Bearer(token) => {
self.content.auth = Auth {
auth_type: Some("bearer".into()),
raw_auth: Some(token.into()),
}
}
}
}
pub fn cookies(&self) -> Vec<cookie_crate::Cookie> {
let mut cookies = vec![];
for (name, c) in &self.content.cookies {
let mut cookie_builder = cookie_crate::Cookie::build(name, &c.value);
if let Some(expires) = c.expires {
cookie_builder =
cookie_builder.expires(time::OffsetDateTime::from_unix_timestamp(expires));
}
if let Some(ref path) = c.path {
cookie_builder = cookie_builder.path(path);
}
if let Some(secure) = c.secure {
cookie_builder = cookie_builder.secure(secure);
}
cookies.push(cookie_builder.finish());
}
cookies
}
pub fn save_cookies(&mut self, cookies: Vec<cookie_crate::Cookie>) {
self.content.cookies.clear();
for cookie in cookies {
self.content.cookies.insert(
cookie.name().into(),
Cookie {
value: cookie.value().into(),
expires: cookie
.expires()
.and_then(|v| v.datetime())
.map(|v| v.unix_timestamp()),
path: cookie.path().map(Into::into),
secure: cookie.secure(),
},
);
}
}
pub fn persist(&self) -> Result<()> {
if !self.path.exists() || !self.read_only {
if let Some(parent_path) = self.path.parent() {
fs::create_dir_all(parent_path)?;
}
let mut session_file = fs::File::create(&self.path)?;
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut ser = serde_json::Serializer::with_formatter(&mut session_file, formatter);
self.content.serialize(&mut ser)?;
session_file.write_all(b"\n")?;
}
Ok(())
}
}
fn xh_version() -> String {
if test_mode() {
"0.0.0".into()
} else {
env!("CARGO_PKG_VERSION").into()
}
}
fn is_path(value: &OsString) -> bool {
value.to_string_lossy().contains(std::path::is_separator)
}
fn path_from_url(url: &Url) -> Result<String> {
match (url.host_str(), url.port()) {
(Some("."), _) | (Some(".."), _) | (None, _) => {
Err(anyhow!("couldn't extract host from url"))
}
(Some(host), Some(port)) => Ok(format!("{}_{}", host, port)),
(Some(host), None) => Ok(host.into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::random_string;
use anyhow::Result;
#[test]
fn can_read_httpie_session_file() -> Result<()> {
let mut path_to_session = std::env::temp_dir();
let file_name = random_string();
path_to_session.push(file_name);
fs::write(
&path_to_session,
indoc::indoc! {r#"
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.org/doc#sessions",
"httpie": "2.3.0"
},
"auth": {
"password": null,
"type": null,
"username": null
},
"cookies": {
"__cfduid": {
"expires": 1620239688,
"path": "/",
"secure": false,
"value": "d090ada9c629fc7b8bbc6dba3dde1149d1617647688"
}
},
"headers": {
"hello": "world"
}
}
"#},
)?;
let session = Session::load_session(
&Url::parse("http://localhost")?,
path_to_session.into(),
false,
)?;
assert_eq!(
session.content.headers.get("hello"),
Some(&"world".to_string()),
);
assert_eq!(
session.content.auth,
Auth {
auth_type: None,
raw_auth: None
},
);
let expected_cookie = serde_json::from_str::<Cookie>(
r#"
{
"expires": 1620239688,
"path": "/",
"secure": false,
"value": "d090ada9c629fc7b8bbc6dba3dde1149d1617647688"
}
"#,
)?;
assert_eq!(
session.content.cookies.get("__cfduid"),
Some(&expected_cookie)
);
Ok(())
}
#[test]
fn can_read_xh_session_file() -> Result<()> {
let mut path_to_session = std::env::temp_dir();
let file_name = random_string();
path_to_session.push(file_name);
fs::write(
&path_to_session,
indoc::indoc! {r#"
{
"__meta__": {
"about": "xh session file",
"httpie": "0.10.0"
},
"auth": {
"raw_auth": "secret-token",
"type": "bearer"
},
"cookies": {
"__cfduid": {
"expires": 1620239688,
"path": "/",
"secure": false,
"value": "d090ada9c629fc7b8bbc6dba3dde1149d1617647688"
}
},
"headers": {
"hello": "world"
}
}
"#},
)?;
let session = Session::load_session(
&Url::parse("http://localhost")?,
path_to_session.into(),
false,
)?;
assert_eq!(
session.content.headers.get("hello"),
Some(&"world".to_string()),
);
assert_eq!(
session.content.auth,
Auth {
auth_type: Some("bearer".into()),
raw_auth: Some("secret-token".into())
},
);
let expected_cookie = serde_json::from_str::<Cookie>(
r#"
{
"expires": 1620239688,
"path": "/",
"secure": false,
"value": "d090ada9c629fc7b8bbc6dba3dde1149d1617647688"
}
"#,
)?;
assert_eq!(
session.content.cookies.get("__cfduid"),
Some(&expected_cookie)
);
Ok(())
}
}