blob: 8cc1f178c78b99bc7a0f1dcce2b171dfbb2189af [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use super::Plugin;
use crate::{
component::COMPONENT_PHP_CURL_ID,
context::RequestContext,
execute::{validate_num_args, AfterExecuteHook, BeforeExecuteHook, Noop},
};
use anyhow::Context;
use phper::{
arrays::{InsertKey, ZArray},
functions::call,
values::{ExecuteData, ZVal},
};
use skywalking::trace::{propagation::encoder::encode_propagation, span::Span};
use std::{cell::RefCell, collections::HashMap, os::raw::c_long};
use tracing::debug;
use url::Url;
static CURLOPT_HTTPHEADER: c_long = 10023;
thread_local! {
static CURL_HEADERS: RefCell<HashMap<i64, ZVal>> = Default::default();
}
#[derive(Default, Clone)]
pub struct CurlPlugin;
impl Plugin for CurlPlugin {
#[inline]
fn class_names(&self) -> Option<&'static [&'static str]> {
None
}
#[inline]
fn function_name_prefix(&self) -> Option<&'static str> {
Some("curl_")
}
fn hook(
&self, _class_name: Option<&str>, function_name: &str,
) -> Option<(Box<BeforeExecuteHook>, Box<AfterExecuteHook>)> {
match function_name {
"curl_setopt" => Some(self.hook_curl_setopt()),
"curl_setopt_array" => Some(self.hook_curl_setopt_array()),
"curl_exec" => Some(self.hook_curl_exec()),
"curl_close" => Some(self.hook_curl_close()),
_ => None,
}
}
}
impl CurlPlugin {
fn hook_curl_setopt(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(|_, execute_data| {
validate_num_args(execute_data, 3)?;
let cid = Self::get_resource_id(execute_data)?;
if matches!(execute_data.get_parameter(1).as_long(), Some(n) if n == CURLOPT_HTTPHEADER)
{
let value = execute_data.get_parameter(2);
if value.get_type_info().is_array() {
CURL_HEADERS
.with(|headers| headers.borrow_mut().insert(cid, value.clone()));
}
}
Ok(Box::new(()))
}),
Noop::noop(),
)
}
fn hook_curl_setopt_array(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(|_, execute_data| {
validate_num_args(execute_data, 2)?;
let cid = Self::get_resource_id(execute_data)?;
if let Some(opts) = execute_data.get_parameter(1).as_z_arr() {
if let Some(value) = opts.get(CURLOPT_HTTPHEADER as u64) {
CURL_HEADERS
.with(|headers| headers.borrow_mut().insert(cid, value.clone()));
}
}
Ok(Box::new(()))
}),
Noop::noop(),
)
}
fn hook_curl_exec(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(|request_id, execute_data| {
validate_num_args(execute_data, 1)?;
let cid = Self::get_resource_id(execute_data)?;
let ch = execute_data.get_parameter(0);
let result = call("curl_getinfo", &mut [ch.clone()])?;
let result = result.as_z_arr().context("result isn't array")?;
let url = result
.get("url")
.context("Get url from curl_get_info result failed")?;
let raw_url = url.as_z_str().context("url isn't string")?.to_str()?;
let mut url = raw_url.to_string();
if !url.contains("://") {
url.insert_str(0, "http://");
}
let url: Url = url.parse().context("parse url")?;
if url.scheme() != "http" && url.scheme() != "https" {
return Ok(Box::new(()));
}
debug!("curl_getinfo get url: {}", &url);
let host = match url.host_str() {
Some(host) => host,
None => return Ok(Box::new(())),
};
let port = match url.port() {
Some(port) => port,
None => match url.scheme() {
"http" => 80,
"https" => 443,
_ => 0,
},
};
let peer = &format!("{host}:{port}");
let mut span = RequestContext::try_with_global_ctx(request_id, |ctx| {
Ok(ctx.create_exit_span(url.path(), peer))
})?;
span.with_span_object_mut(|span| {
span.component_id = COMPONENT_PHP_CURL_ID;
span.add_tag("url", raw_url);
});
let sw_header = RequestContext::try_with_global_ctx(request_id, |ctx| {
Ok(encode_propagation(ctx, url.path(), peer))
})?;
let mut val = CURL_HEADERS
.with(|headers| headers.borrow_mut().remove(&cid))
.unwrap_or_else(|| ZVal::from(ZArray::new()));
if let Some(arr) = val.as_mut_z_arr() {
arr.insert(
InsertKey::NextIndex,
ZVal::from(format!("sw8: {}", sw_header)),
);
let ch = execute_data.get_parameter(0);
call(
"curl_setopt",
&mut [ch.clone(), ZVal::from(CURLOPT_HTTPHEADER), val],
)?;
}
Ok(Box::new(span))
}),
Box::new(move |_, span, execute_data, _| {
let mut span = span.downcast::<Span>().unwrap();
let ch = execute_data.get_parameter(0);
let result = call("curl_getinfo", &mut [ch.clone()])?;
let response = result.as_z_arr().context("response in not arr")?;
let http_code = response
.get("http_code")
.and_then(|code| code.as_long())
.context("Call curl_getinfo, http_code is null")?;
span.add_tag("status_code", &*http_code.to_string());
if http_code == 0 {
let result = call("curl_error", &mut [ch.clone()])?;
let curl_error = result
.as_z_str()
.context("curl_error is not string")?
.to_str()?;
span.with_span_object_mut(|span| {
span.is_error = true;
span.add_log(vec![("CURL_ERROR", curl_error)]);
});
} else if http_code >= 400 {
span.with_span_object_mut(|span| span.is_error = true);
} else {
span.with_span_object_mut(|span| span.is_error = false);
}
Ok(())
}),
)
}
fn hook_curl_close(&self) -> (Box<BeforeExecuteHook>, Box<AfterExecuteHook>) {
(
Box::new(|_, execute_data| {
validate_num_args(execute_data, 1)?;
let cid = Self::get_resource_id(execute_data)?;
CURL_HEADERS.with(|headers| headers.borrow_mut().remove(&cid));
Ok(Box::new(()))
}),
Noop::noop(),
)
}
fn get_resource_id(execute_data: &mut ExecuteData) -> anyhow::Result<i64> {
// The `curl_init` return object since PHP8.
let ch = execute_data.get_parameter(0);
ch.as_z_res()
.map(|res| res.handle())
.or_else(|| ch.as_z_obj().map(|obj| obj.handle().into()))
.context("Get resource id failed")
}
}