Closed resilar closed 4 years ago
Rustilla ja Hyperillä toteutettu h2bridge
joka siltaa osoitteeseen http://127.0.0.1:13337/
HTTP-lähetetyt kutsut HTTP/2 + ALPN yhteyden läpi komentoriviargumentin argv[0]
osoittamalle kohdepalvelimelle (eli tavallisesti ylilauta.org
). Ylilauta-spesifinä logiikkana kohdepalvelimen vastauksista kaivetaan key
-keksi (jos Ylilauta tarjoaa bottitarkistusta) uudelleenlähetystä varten. Viimeisintä key
-arvoa käytetään niin kauan kunnes Ylilauta ohjaa taas bottitarkistussivulle.
Koodi toimii, mutta ei ole nättiä. Hyper teki tästä yllättävän vaikeaa jättämällä Clone
/Copy
traitit toteuttamatta Request
-tyypille joka syödään sen lähetyksessä. Muuten ei olisi ollut mitään ongelmaa, mutta bottitarkistuksen tapauksessa Request
tarvitsee lähettää kahdesti. Paremmassa toteutuksessa h2bridge
olisi geneerinen HTTP/2+ALPN silta ja Ylilautaan liittyvä logiikka olisi jätetty Python-koodille, mutta en ollut tarpeeksi fiksu toteuttamaan tätä heti oikein ensimmäisellä yrityksellä.
Pyfibottiin tätä siltaviritelmää ei ole järkeä mergetä koska kääntäminen ja konffaaminen on kärsimystä Jos Ylilauta-titlet ovat joillekin välttämättömiä, niin h2bridge
toimii väliaikaisratkaisuna toistaiseksi. Paras kuitenkin olisi ettei kenenkään tarvitsisi tätä käyttää ja löytyisi siistimpi ratkaisu jonka pystyisi mergeämään pyfibottiin. Pythonilla HTTP/2+ALPN ovat käsittääkseni aika suuria kysymysmerkkejä ainakin vielä, joten en pidättäisi henkeäni.
[package]
name = "h2bridge"
version = "0.1.0"
authors = ["def <def.github-spam@huumeet.info>"]
edition = "2018"
[dependencies]
futures = "0.3"
hyper = "0.13"
hyper-alpn = "0.2"
hyper-tls = "0.4"
lazy_static = "1.4.0"
regex = "1.3"
tokio = { version = "0.2", features = ["full"] }
use std::pin::Pin;
use std::sync::Mutex;
#[macro_use]
extern crate lazy_static;
use regex::bytes::Regex;
use futures::future::Future;
use hyper::header::{COOKIE, HOST, LOCATION, HeaderValue};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, Server};
static H2BRIDGE_ADDR: &'static str = "127.0.0.1:13337";
type ProxyConnector = hyper_alpn::AlpnConnector;
type ProxyClient = hyper::Client<ProxyConnector>;
fn proxy(mut req: Request<Body>, client: ProxyClient, target: String)
-> Pin<Box<dyn Future<Output=Result<Response<Body>, hyper::Error>> + Send>> {
lazy_static! {
static ref KEY: Mutex<String> = Mutex::new(String::new());
static ref RX_KEY: Regex = Regex::new(r"(key=[0-9a-f]{32});").unwrap();
}
// Redirect request to the target
hyper::http::uri::Builder::new()
.scheme(req.uri().scheme().map_or("https", |s| s.as_str()))
.authority(target.as_str())
.path_and_query(req.uri().path_and_query().map_or("/", |p| p.as_str()))
.build().ok().and_then(|uri| Some(*req.uri_mut() = uri));
HeaderValue::from_str(&target).ok()
.and_then(|value| req.headers_mut().insert(HOST, value));
// Add key cookie to headers
KEY.lock().ok().filter(|key| !key.is_empty())
.and_then(|ref mut key| HeaderValue::from_str(key).ok())
.and_then(|key| req.headers_mut().insert(COOKIE, key));
// Parse responses from GET & HEAD requests only
if !matches!(*req.method(), Method::GET | Method::HEAD) {
return Box::pin(async move { client.request(req).await });
}
Box::pin(async move {
// Build a copy of req (Request: !Copy + !Clone, ffs)
let (parts, body) = req.into_parts();
let req = Request::get(parts.uri.clone())
.method(parts.method.clone())
.version(parts.version);
let req = match req.body(Body::empty()) {
Ok(mut req) => {
// Copy headers (including key cookie)
parts.headers.iter().for_each(|(key, value)| {
req.headers_mut().insert(key, value.clone());
//eprintln!("{:?}: {:?}", key, value);
});
req
},
Err(e) => {
eprintln!("Error building a request to {:?}: {:?}", target, e);
return client.request(Request::from_parts(parts, body)).await;
}
};
// Finally, send the copied request (consuming it)
let res = client.request(req).await?;
let (mut res_parts, res_body) = res.into_parts();
let buf = hyper::body::to_bytes(res_body).await?;
// Update response Location (if HTTP redirecting)
if let Some(uri) = res_parts.headers.get(LOCATION)
.and_then(|u| u.to_str().ok()) {
let new_uri = uri.replace(&target, H2BRIDGE_ADDR)
.replace("https://", "http://");
if let Ok(value) = HeaderValue::from_str(&new_uri) {
res_parts.headers.insert(LOCATION, value);
}
}
// Update key if received a new one
let mut retry = false;
for cap in RX_KEY.captures_iter(&buf) {
if let Some(value) = KEY.lock().ok().and_then(|mut prev_key| {
if &cap[1] != prev_key.as_bytes() {
let key = String::from_utf8_lossy(&cap[1]);
*prev_key = key.to_string();
Some(key)
} else {
None
}
}) {
println!("New cookie: {}", value);
retry = true;
break;
}
}
// Resend if the key cookie changed
if retry {
let req = Request::from_parts(parts, body);
return proxy(req, client, target).await;
}
// Return last response
let res = Response::from_parts(res_parts, Body::from(buf));
async move { Ok(res) }.await
})
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let args = std::env::args();
if args.len() != 2 {
let arg0 = std::env::args().next().unwrap_or("h2bridge".into());
eprintln!("Usage: {} [TARGET_DOMAIN]\n", arg0);
eprintln!(" e.g.: {} ylilauta.org", arg0);
return Ok(());
}
let target = args.last().unwrap();
let addr: std::net::SocketAddr = H2BRIDGE_ADDR.parse().unwrap();
println!("Listening @ {:?} (HTTP/2 bridge to '{:?}')", addr, target);
let client = hyper::Client::builder()
.http2_only(true)
.build(ProxyConnector::new());
let make_svc = make_service_fn(move |_socket: _| {
let target = target.clone();
let client = client.clone();
async move {
Ok::<_, hyper::Error>(service_fn(move |req: _| {
proxy(req, client.clone(), target.clone())
}))
}
});
let server = Server::bind(&addr).serve(make_svc);
let server = server.with_graceful_shutdown(async {
tokio::signal::ctrl_c().await
.expect("failed to install CTRL+C signal handler");
});
if let Err(err) = server.await {
eprintln!("Server error: {}", err);
}
Ok(())
}
[Unit]
Description=Ylilauta.org HTTP/2 bridge
AssertPathExists=%h/h2bridge/target/release
[Service]
Type=exec
ExecStart=%h/h2bridge/target/release/h2bridge ylilauta.org
[Install]
WantedBy=multi-user.target
EOF
diff --git a/pyfibot/modules/module_urltitle.py b/pyfibot/modules/module_urltitle.py
index 1a9152d..3b8eaa2 100644
--- a/pyfibot/modules/module_urltitle.py
+++ b/pyfibot/modules/module_urltitle.py
@@ -1613,0 +1614,9 @@ def _handle_gfycat(url):
+def _handle_ylilauta(url):
+ """http*://*ylilauta.org*"""
+ old = url
+ url = url.replace("https://", "http://")
+ url = url.replace("ylilauta.org", "127.0.0.1:13337")
+ log.info("Mapping Ylilauta.org URL to h2bridge: %s -> %s", old, url)
+ return __get_title_tag(url)
+
+
Python HTTP-kirjastojen HTTP/2-tuki on keskimäärin heikkoa eikä ALPN ole itsestäänselvyys. Nykyisillä pyfibotin riippuvuuksilla ei taida onnistua (ALPN kyllä määrittämällä HTTP-yhteydelle soketin tai SSLContextin, mutta HTTP/2 puuttuu). Pari vaihtoehtoa löytyy uusilla riippuvuuksilla.
pycurl
on ilmeisin vaihtoehto koska curl
komentorivillä taipuu tähän (kuten esimerkissä) ja kaiken pitäisi toimia setopt(CURL_HTTP_VERSION_2TLS)
jälkeen (ALPN on option CURLOPT_SSL_ENABLE_ALPN
takana, mutta oletuksena päällä). pycurl
ei kai sisällä omaa libcurl
ia, joten lisävaatimuksena on HTTP/2-tuen (ja ALPN-tuen) sisältävä libcurl >=7.47.0
(julkaistu 2016 tammikuussa); esimerkiksi Debian Jessie (8.11) sisältää liian vanhan version.
hyper
oli mennyt minulta aikaisemmin ohi, mutta näyttäisi tukevan HTTP/2 sekä ALPN on oletuksena käytössä HTTPS-yhteyksille. Python >=2.7.9 (tai >=3.4) vaaditaan, eli Debian Jessielläkin pitäisi toimia. Kieltämättä hyper
näyttäisi lupaavalta ratkaisulta ja Ylilauta-titleille tuki pyfibottiin ei ehkä vaadikaan liikoja. hyper
riippuvuus ja tusina riviä key
-keksin pyörittelyyn varmaan riittäisi. Ei paha.
Jos hyper
riippuvuus on OK, niin lähetän PR:n joskus paremmalla ajalla ellei joku muu innostu ensin.
Hyper tuntuu olevan vähän yliherkkä sisääntulevan datan validoinnin kanssa:
from hyper.contrib import HTTP20Adapter
import requests
s = requests.Session()
s.stream = True
s.mount("https://ylilauta.org/", HTTP20Adapter())
s.headers.update(
{"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:49.0) Gecko/20100101 Firefox/49.0"})
s.headers.update({"Accept-Language": "*"})
s.headers.update(
{"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"})
response = s.get("https://ylilauta.org/arkisto/122796229")
print(response)
Tämä leviää näin: h2.exceptions.ProtocolError: Received header value surrounded by whitespace
, koska connect-src -headerissa on joko tarkoituksella tai vahingossa whitespacea Hyperin mielestä väärä määrä.
Ja tämän takia missään vaiheessa ei edes päästä kaivelemaan palautuvaa Githubissues.
Taitaa merkittävä osa pyfibotin käyttäjistä olla suomalaisia, joten Ylilaudan URLit olisi ihan kiva. Ylilauta on kuitenkin tehnyt titlejen hakemisesta mahdollisimman vaikeeta käsittääkseni bottien kitkemiseen perffisyistä. Selvitin mistä hulavanteista on tarkalleen hypittävä, että Ylilauta suostuu antamaan titlen.
Esivaatimuksena TLS-kättelyssä ALPN:llä HTTP/2-protokollan neuvotteleva HTTP-kirjasto. Väärä HTTP-versio tai ALPN puuttuu ⇒ Ylilaudan palvelin katkaisee yhteyden TLS-kättelyn jälkeen.
Ensimmäinen lataus on antaa "Klikkaa nappia niin tiedämme ettet ole botti" sivun, jonka nappi asettaa
key
-keksin. Keksin arvon voi poimia sivun sisällöstä triviaalisti vaikkapa regexeillä:Keksi
key
headereihin ja Ylilauta antaa ystävällisesti titlen.Pyfibotin kannalta haastavinta on HTTP/2-kutsujen lähettäminen ja HTTP-version sopiminen ALPN:llä. Nopealla vilkaisulla Python-webbikirjastojen HTTP/2 ja ALPN -tuki on olematonta.
WONTFIX
on varsin perusteltua enkä väitä vastaan, mutta ylemmäncurl
ratkaisun dokumentoinnista tänne voi olla jollekin hyötyä (issue tracker ei ehkä paras paikka, mutta en näin neljältä aamuyöstä keksinyt parempaakaan). Seuraavassa kommentissa ei-mergekelpoinen purkkaratkaisu pyfibotille, jos joku muukin tarvitsee Ylilauta-titlejä yhtä epätoivoisesti kuin minä.@Sopsy vois kans yksinkertaisen APIn tarjoamista titlejen hakuun (miksei muuhunkin) ellei se ole liian vaikeeta. Vaikka sitten POST-requilla toimivan, niin ei pitäisi bottienkaan crawlailla niin helposti kuin REST-genkistä GET APIa. Jos joku API on jo olemassa, niin sivistäkää minua.