Skip to content

Instantly share code, notes, and snippets.

@Tosainu
Created February 6, 2022 09:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Tosainu/006267e5da4a3f361bbafa6ef58940db to your computer and use it in GitHub Desktop.
Save Tosainu/006267e5da4a3f361bbafa6ef58940db to your computer and use it in GitHub Desktop.
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cc"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "curl"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de97b894edd5b5bcceef8b78d7da9b75b1d2f2f9a910569d0bde3dd31d84939"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"winapi",
]
[[package]]
name = "curl-sys"
version = "0.4.52+curl-7.81.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14b8c2d1023ea5fded5b7b892e4b8e95f70038a421126a056761a84246a28971"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"winapi",
]
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "getrandom"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74"
[[package]]
name = "libz-sys"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "openssl"
version = "0.10.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-sys",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.17.0+1.1.1m"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d6a336abd10814198f66e2a91ccd7336611f30334119ca8ce300536666fcf4"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb"
dependencies = [
"autocfg",
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "pkg-config"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "rand"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
]
[[package]]
name = "redox_users"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [
"getrandom",
"redox_syscall",
]
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi",
]
[[package]]
name = "socket2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "t"
version = "0.1.0"
dependencies = [
"curl",
"dirs",
"openssl",
"rand",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[package]
name = "t"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "t"
path = "main.rs"
[dependencies]
curl = "0.4.42"
dirs = "4.0.0"
openssl = { version = "0.10.38", features = ["vendored"] }
rand = "0.8.4"
use std::io::{BufRead, Read};
fn main() -> t::Result<()> {
let [consumer_key, consumer_secret, access_token, access_secret] = {
let cfg_file = dirs::config_dir()
.ok_or_else(|| t::Error::String("$HOME or $XDG_CONFIG_FIR may not set".to_owned()))?
.join("t")
.join("config");
let cfg = std::fs::OpenOptions::new().read(true).open(cfg_file)?;
let mut cfg = std::io::BufReader::new(cfg);
let mut lines = [String::new(), String::new(), String::new(), String::new()];
for line in &mut lines {
cfg.read_line(line)?;
}
lines
};
let oauth = t::Oauth::new(
consumer_key.trim_end(),
consumer_secret.trim_end(),
access_token.trim_end(),
access_secret.trim_end(),
);
let tweet = if let Some(tweet) = std::env::args().nth(1) {
tweet
} else {
let mut tweet = String::new();
std::io::stdin().read_to_string(&mut tweet)?;
tweet
};
t::post(
"https://api.twitter.com/1.1/statuses/update.json",
&oauth,
&[("status", &tweet)],
)
}
mod t {
use std::collections::BTreeMap;
#[derive(Debug)]
pub struct Oauth<'a> {
consumer_key: &'a str,
consumer_secret: &'a str,
access_token: &'a str,
access_secret: &'a str,
}
impl<'a> Oauth<'a> {
pub fn new(
consumer_key: &'a str,
consumer_secret: &'a str,
access_token: &'a str,
access_secret: &'a str,
) -> Oauth<'a> {
Oauth {
consumer_key,
consumer_secret,
access_token,
access_secret,
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum RequestType {
Get,
Post,
}
#[allow(dead_code)]
pub fn get(url: &str, oauth: &Oauth, params: &[(&str, &str)]) -> Result<()> {
req(RequestType::Get, url, oauth, params)
}
#[allow(dead_code)]
pub fn post(url: &str, oauth: &Oauth, params: &[(&str, &str)]) -> Result<()> {
req(RequestType::Post, url, oauth, params)
}
fn req(reqtype: RequestType, url: &str, oauth: &Oauth, params: &[(&str, &str)]) -> Result<()> {
let auth_header = oauth_header(reqtype, url, oauth, params)?;
let mut header = curl::easy::List::new();
header.append(&auth_header)?;
let query = query_str(&BTreeMap::from_iter(params.iter().cloned()));
let mut c = curl::easy::Easy::new();
if query.is_empty() {
c.url(url)?;
} else {
let url = format!("{}?{}", url, query);
c.url(&url)?;
}
match reqtype {
RequestType::Get => c.get(true)?,
RequestType::Post => c.post(true)?,
}
c.http_headers(header)?;
c.write_function(|data| {
use std::io::Write;
std::io::stdout().write_all(data).unwrap();
Ok(data.len())
})?;
c.perform()?;
Ok(())
}
fn oauth_header(
reqtype: RequestType,
url: &str,
oauth: &Oauth,
params: &[(&str, &str)],
) -> Result<String> {
let mut auth_params = auth_params(oauth);
for (k, v) in params.iter() {
auth_params.insert(k.to_string(), v.to_string());
}
let signature_key = PercentEncoder::new(oauth.consumer_secret.as_bytes().iter())
.chain(std::iter::once('&'))
.chain(PercentEncoder::new(oauth.access_secret.as_bytes().iter()))
.collect::<String>();
let signature_base = {
let query = query_str(&auth_params);
match reqtype {
RequestType::Get => "GET&".chars(),
RequestType::Post => "POST&".chars(),
}
.chain(PercentEncoder::new(url.as_bytes().iter()))
.chain(std::iter::once('&'))
.chain(PercentEncoder::new(query.as_bytes().iter()))
.collect::<String>()
};
auth_params.insert(
"oauth_signature".to_owned(),
hmac_sha1(signature_key.as_bytes(), signature_base.as_bytes())?,
);
let header_params = {
let mut s = auth_params
.iter()
.flat_map(|(k, v)| {
k.chars()
.chain(std::iter::once('='))
.chain(std::iter::once('"'))
.chain(PercentEncoder::new(v.as_bytes().iter()))
.chain(std::iter::once('"'))
.chain(std::iter::once(','))
})
.collect::<String>();
s.pop();
s
};
Ok(format!("{} {}", "Authorization: OAuth", header_params))
}
fn auth_params(oauth: &Oauth) -> BTreeMap<String, String> {
// https://datatracker.ietf.org/doc/html/rfc5849#section-3.1
BTreeMap::from([
(
"oauth_consumer_key".to_owned(),
oauth.consumer_key.to_owned(),
),
("oauth_token".to_owned(), oauth.access_token.to_owned()),
("oauth_signature_method".to_owned(), "HMAC-SHA1".to_owned()),
("oauth_timestamp".to_owned(), oauth_timestamp()),
("oauth_nonce".to_owned(), oauth_nonce()),
("oauth_version".to_owned(), "1.0".to_owned()),
])
}
fn oauth_timestamp() -> String {
use std::time::SystemTime;
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
.to_string()
}
fn oauth_nonce() -> String {
use rand::Rng;
rand::thread_rng()
.sample_iter(rand::distributions::Alphanumeric)
.take(16)
.map(|c| c as char)
.collect()
}
fn query_str<T: AsRef<str>>(params: &BTreeMap<T, T>) -> String {
let mut s = params
.iter()
.flat_map(|(k, v)| {
k.as_ref()
.chars()
.chain(std::iter::once('='))
.chain(PercentEncoder::new(v.as_ref().as_bytes().iter()))
.chain(std::iter::once('&'))
})
.collect::<String>();
s.pop(); // trim last b'&'
s
}
fn hmac_sha1(key: &[u8], data: &[u8]) -> Result<String> {
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Signer;
let pkey = PKey::hmac(key)?;
let sig = Signer::new(MessageDigest::sha1(), &pkey)?.sign_oneshot_to_vec(data)?;
Ok(openssl::base64::encode_block(&sig))
}
#[derive(Debug)]
pub enum Error {
String(String),
Io(std::io::Error),
Curl(curl::Error),
OpenSSL(openssl::error::ErrorStack),
}
pub type Result<T> = std::result::Result<T, Box<Error>>;
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::String(s) => s.fmt(f),
Error::Io(e) => e.fmt(f),
Error::Curl(e) => e.fmt(f),
Error::OpenSSL(e) => e.fmt(f),
}
}
}
impl From<std::io::Error> for Box<Error> {
fn from(e: std::io::Error) -> Box<Error> {
Box::new(Error::Io(e))
}
}
impl From<curl::Error> for Box<Error> {
fn from(e: curl::Error) -> Box<Error> {
Box::new(Error::Curl(e))
}
}
impl From<openssl::error::ErrorStack> for Box<Error> {
fn from(e: openssl::error::ErrorStack) -> Box<Error> {
Box::new(Error::OpenSSL(e))
}
}
struct PercentEncoder<I> {
iter: I,
state: PercentEncoderState,
}
impl<I> PercentEncoder<I> {
pub fn new(iter: I) -> PercentEncoder<I> {
PercentEncoder {
iter,
state: PercentEncoderState::ReadNext,
}
}
}
#[derive(Debug)]
enum PercentEncoderState {
ReadNext,
HexUpper(u8),
HexLower(u8),
}
// https://datatracker.ietf.org/doc/html/rfc5849#section-3.6
impl<'a, I: Iterator<Item = &'a u8>> Iterator for PercentEncoder<I> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
let hex = |v| match v {
s @ 0x0..=0x9 => Some(b'0' + s),
s @ 0xa..=0xf => Some(b'A' - 0xa + s),
_ => None,
};
match self.state {
PercentEncoderState::ReadNext => match self.iter.next().cloned() {
sc @ Some(b'-' | b'.' | b'_' | b'~') => sc,
sc @ Some(c) if c.is_ascii_alphanumeric() => sc,
Some(c) => {
self.state = PercentEncoderState::HexUpper(c);
Some(b'%')
}
None => None,
},
PercentEncoderState::HexUpper(c) => {
self.state = PercentEncoderState::HexLower(c);
hex((c >> 4) & 0xf)
}
PercentEncoderState::HexLower(c) => {
self.state = PercentEncoderState::ReadNext;
hex(c & 0x0f)
}
}
.map(|c| c as char)
}
}
#[test]
fn percent_encode() {
let s = "abc-._~ <>123".to_string();
let e = PercentEncoder::new(s.as_bytes().iter()).collect::<String>();
assert_eq!(&e, "abc-._~%20%3C%3E123");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment