Open8

RustのTIPS

青柳康平青柳康平

スレッドの中で乱数を使う。

rand = "0.8.5"
use rand::{Rng, SeedableRng};
let mut rng = rand::rngs::StdRng::from_entropy();
let res = rng.gen::<u8>();
青柳康平青柳康平

minioを使ってS3にアクセス

let credentials_provider = Credentials::new("admin123", "admin123", None, None, "example");
let config = aws_sdk_s3::Config::builder()
    .behavior_version_latest()
    .credentials_provider(credentials_provider)
    .region(Region::new("ap-northeast-1"))
    .force_path_style(true)
    .endpoint_url("http://minio:9000")
    .build();
Client::from_conf(config);
青柳康平青柳康平

AESとCTRのサンプル

main.rs
use aes::cipher::StreamCipher;
use ctr::cipher::KeyIvInit;
type Aes128Ctr64LE = ctr::Ctr64LE<aes::Aes128>;

// 暗号化
fn encrypt(key: &str, data: &str) -> Vec<u8> {
    use rand::{Rng, SeedableRng};
    let mut rng = rand::rngs::StdRng::from_entropy();
    let iv = rng.gen::<[u8; 16]>();
    let mut cipher = Aes128Ctr64LE::new(key.as_bytes().into(), &iv.into());
    let mut buf = data.as_bytes().to_vec();
    cipher.apply_keystream(&mut buf);
    let mut res = iv.to_vec();
    res.extend(buf);
    res
}

// 復号化
fn decrypt(key: &str, data: &Vec<u8>) -> String {
    let mut cipher = Aes128Ctr64LE::new(key.as_bytes().into(), data[0..16].into());
    let mut buf2: Vec<u8> = vec![0; data.len() - 16];
    if let Err(err) = cipher.apply_keystream_b2b(&data[16..], &mut buf2.as_mut_slice()) {
        println!("{}", err);
    }
    String::from_utf8(buf2.to_vec()).unwrap()
}

fn main() {
    let plaintext = "予定表~①💖ハンカクだ";
    let key = "0123456701234567";
    let enc = encrypt(key, plaintext);
    let dec = decrypt(key, &enc);
    assert_eq!(plaintext, dec);
    println!("{}", dec);
}
青柳康平青柳康平

CloudflareでOAuth2

Cargo.toml
[package]
name = "cms"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
axum  = { version = "0.7", default-features = false, features = ['query', 'json'] }
console_error_panic_hook = { version = "0.1.1" }
tower-cookies = "0.10.0"
tower-service = "0.3.2"
twapi-v2 = { version = "0.14", features = ["rustls-tls", "oauth"], default-features = false }
worker = { version = "0.1.0", features=['http', 'axum'] }
worker-macros = { version = "0.1.0", features = ['http'] }


[profile.release]
opt-level = "s" # optimize for size in release builds
lto = true
strip = true
codegen-units = 1
lib.rs
use std::{collections::HashMap, sync::Arc};

use axum::{extract::{Query, State}, response::{Html, IntoResponse, Json}, routing::get, Router};
use tower_cookies::{Cookie, CookieManagerLayer, Cookies};
use tower_service::Service;
use twapi_v2::{api::{get_2_users_me, BearerAuthentication}, oauth::{TwitterOauth, TwitterScope}};
use worker::*;

pub const PKCE_VERIFIER: &str = "pkce_verifier";

pub struct AppState {
    pub twitter_oauth: TwitterOauth,
}

fn router(env: &Env) -> Router {
    let twitter_oauth = oauth_client(&env).unwrap();
    let app_store = Arc::new(AppState { twitter_oauth });
    Router::new()
        .route("/", get(root))
        .route("/oauth", get(oauth))
        .with_state(app_store)
        .layer(CookieManagerLayer::new())
}

#[event(fetch)]
async fn fetch(
    req: HttpRequest,
    env: Env,
    _ctx: Context,
) -> Result<axum::http::Response<axum::body::Body>> {
    console_error_panic_hook::set_once();
    Ok(router(&env).call(req).await?)
}

fn oauth_client(env: &Env) -> Result<TwitterOauth> {
    TwitterOauth::new(
        &env.secret("CLIENT_ID").unwrap().to_string(),
        &env.secret("CLIENT_SECRET").unwrap().to_string(),
        &env.var("CALLBACK_URL").unwrap().to_string(),
        TwitterScope::all(),
    ).map_err(|e| Error::RustError(e.to_string()))
}

pub async fn root(cookies: Cookies, State(state): State<Arc<AppState>>,) -> impl IntoResponse {
    let res = state.twitter_oauth.oauth_url();
    cookies.add(Cookie::new(PKCE_VERIFIER, res.pkce_verifier.clone()));
    Html(format!("<a href='{}'>oauth<a>", res.oauth_url)).into_response()
}

#[worker::send]
async fn oauth(cookies: Cookies, State(state): State<Arc<AppState>>, Query(params): Query<HashMap<String, String>>, ) -> impl IntoResponse {
    let pkce = cookies.get(PKCE_VERIFIER).unwrap();
    let code  = params.get("code").unwrap();
    let res = state.twitter_oauth
        .token(pkce.value(), code)
        .await
        .unwrap();
    println!("{:?}", res);
    let auth = BearerAuthentication::new(res.access_token);
    let me = get_2_users_me::Api::all().execute(&auth).await.unwrap();
    Json(me.0).into_response()
}
青柳康平青柳康平

GitHubのprivateを参照する方法

VSCodeのrust-analyzerと開発環境のdocker内で利用できるようにする。

Cargo.toml

Cargo.toml
[net]
git-fetch-with-cli = true

[dependencies]
your-crate = { git = "ssh://git@github.com/MyOrg/your-crate.git", branch = "main"}

GitHubからsshのリポジトリをコピーするが、先頭にssh://をつけるのと、ホスト名と組織名が「:」で区切られているので/に変更する。

rust-analyzer

VSCodeのworkspaceファイルに追加する

{
  "settings": {
    "rust-analyzer.server.extraEnv": {
      "CARGO_NET_GIT_FETCH_WITH_CLI": "true"
    },
  }
}

Docker

環境変数

CARGO_NET_GIT_FETCH_WITH_CLI=true
GIT_SSH_COMMAND='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'

docker-compose

docker-compose.yaml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.app
    secrets:
      - host_ssh_key

secrets:
  host_ssh_key:
    file: ~/.ssh/id_ed25519
Dockerfil.app
FROM rust:1.80
RUN mkdir ~/.ssh && ln -s /run/secrets/host_ssh_key ~/.ssh/id_ed25519
青柳康平青柳康平

axumのmultipartをファイルに保存する

use tokio::fs::File;
use tokio_util::io::StreamReader;
use futures_util::TryStreamExt;

while let Some(field) = multipart.next_field().await? {
    if let Some(content_type) = field.headers().get("content-type") {
        if content_type == "text/csv" {
            let mut body_reader = StreamReader::new(field.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
            let mut file = File::create(file_path.clone()).await?;
            tokio::io::copy(&mut body_reader, &mut file).await?;
        }
    }
}
青柳康平青柳康平

CloudflareのAI Getawayを通してAmazon Bedrockを呼び出す

Cargo.toml
[package]
name = "bedrock"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
aws-config = { version = "1.5.6", features = ["behavior-version-latest"] }
aws-sdk-bedrockruntime = "1.50.0"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
http = "1.1.0"
main.rs
use aws_config::{BehaviorVersion, Region};
use aws_sdk_bedrockruntime::{
    config::{
        interceptors::BeforeTransmitInterceptorContextMut, ConfigBag, Intercept, RuntimeComponents,
    },
    error::BoxError,
    types::{ContentBlock, ConversationRole, Message},
    Config,
};
use http::Uri;

const HOST_NAME: &str = "gateway.ai.cloudflare.com";
const REGINO_NAME: &str = "us-east-1";
const MODEL_ID: &str = "anthropic.claude-3-5-sonnet-20240620-v1:0";
const CONTENTS: &str = "次の内容を英語にしてください。「こんにちは世界」";

fn make_uri(uri: &str) -> Result<String, BoxError> {
    let uri: Uri = uri.parse()?;
    Ok(format!(
        "https://{}/v1/{}/{}/aws-bedrock/bedrock-runtime/{}{}",
        HOST_NAME,
        std::env::var("CF_ID").unwrap_or_default(),
        std::env::var("CF_NAME").unwrap_or_default(),
        REGINO_NAME,
        uri.path()
    ))
}

#[derive(Debug)]
pub struct UriModifierInterceptor;
impl Intercept for UriModifierInterceptor {
    fn name(&self) -> &'static str {
        "UriModifierInterceptor"
    }
    fn modify_before_transmit(
        &self,
        context: &mut BeforeTransmitInterceptorContextMut<'_>,
        _runtime_components: &RuntimeComponents,
        _cfg: &mut ConfigBag,
    ) -> Result<(), BoxError> {
        let request = context.request_mut();
        let headers = request.headers_mut();
        headers.append("host", HOST_NAME);
        *request.headers_mut() = headers.clone();
        request.set_uri(make_uri(request.uri())?)?;

        Ok(())
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let config = Config::builder()
        .behavior_version(BehaviorVersion::latest())
        .interceptor(UriModifierInterceptor)
        .region(Region::new(REGINO_NAME))
        .credentials_provider(aws_config::default_provider::credentials::default_provider().await)
        .build();
    let client = aws_sdk_bedrockruntime::Client::from_conf(config);
    let res = client
        .converse()
        .model_id(MODEL_ID)
        .messages(
            Message::builder()
                .role(ConversationRole::User)
                .content(ContentBlock::Text(CONTENTS.to_string()))
                .build()
                .map_err(|_| "failed to build message")
                .unwrap(),
        )
        .send()
        .await?;
    println!("{:?}", res);

    Ok(())
}
青柳康平青柳康平

Google Driveにファイルをアップロードする。
参考Rust rustlsのpanic"no process-level CryptoProvider available -- call CryptoProvider::install_default"

Cargo.toml
[package]
name = "drive"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
http-body-util = "0.1.2"
hyper-rustls = { version = "0.27.3", feaures = ["native-tokio", "http1", "tls12", "logging", "ring"], default-features = false }
google-drive3 = "6.0.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"]}
serde_json = "1"
main.rs
use google_drive3::{
    api::File, hyper_util, yup_oauth2::{self, ServiceAccountKey}, DriveHub
};
use http_body_util::BodyExt;

// RUST_BACKTRACE=1 cargo run
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let secret = tokio::fs::read_to_string("secret.json").await?;
    let secret: ServiceAccountKey = serde_json::from_str(&secret)?;
    let auth = yup_oauth2::ServiceAccountAuthenticator::builder(secret)
        .build()
        .await
        .unwrap();

    let client = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
        .build(
            hyper_rustls::HttpsConnectorBuilder::new()
                .with_native_roots()
                .unwrap()
                .https_or_http()
                .enable_http1()
                .build(),
        );
    let hub = DriveHub::new(client, auth);
    
    // アップロードは直接指定のフォルダーに権限を設定する
    let req = File {
        name: Some("test3.csv".to_string()),
        mime_type: Some("text/csv".to_string()),
        parents: Some(vec!["置きたいフォルダーのID".to_string()]),
        ..Default::default()
    };

     let result = hub
        .files()
        .create(req)
        .supports_all_drives(true)
        .upload(std::fs::File::open("test.csv").unwrap(), "text/csv".parse().unwrap())
        .await?;
    println!("{:?}", result);
    let body: Vec<u8> = result.0.into_body().collect().await?.to_bytes().into();
    let body = String::from_utf8(body).unwrap();
    let body: serde_json::Value = serde_json::from_str(&body)?;
    println!("{:?}", body);
     

    Ok(())
}