AWS Lambda와 Tumblr로 교차 결제

나는 정말 making a CLI tool to automate cross-posting blog posts to different places을 좋아한다.그래서 나는 다른 플랫폼을 찾기 시작했다. 텀블러는 좋은 후보인 것 같다.마찬가지로 대략적인 코드는 on Github이다.

Tumblr API


Tumblr API는 well documented입니다.하지만 시작하기 전에 register an "application"을 알아야 한다.기본 콜백 URL의 경우 나중에 언제든지 편집할 수 있는 유효한 URL을 입력합니다(계정 > 설정 > 적용 >✎). 이것은 OAuth "소비자"키와 기밀 (때로는 "클라이언트"증빙서류라고 부른다) 을 얻을 수 있습니다.
Tumblr API의 일부 부분은 사용자 키로만 호출할 수 있습니다.예: retrieve published posts:
curl -G https://api.tumblr.com/v2/blog/{blog-identifier}/posts?api_key={oauth_consumer_key}
# PowerShell Example:
Invoke-WebRequest https://api.tumblr.com/v2/blog/rendered-obsolete/posts?api_key=xxx
그러나 대다수의 재미있는 것, 예를 들면 creating a new post은 모두 OAuth가 필요하다.

비통조직


Tumblr의 docs은 OAuth 1.0a의 세부 사항에 대해 조금 알고 있지만 이들의 실현은 매우 유사하다. 또 하나는 RFC이다.트위터는'세 다리'OAuth 인증, RFC는'리디렉션 기반'인증이라고 부른다.네가 그것을 어떻게 부르든지 간에 요점은:
  • POST https://www.tumblr.com/oauth/request_token 클라이언트 자격 증명(즉, 소비자 키/기밀) -> 임시 자격 증명 수신
  • 직접 사용자 - https://www.tumblr.com/oauth/authorize 웹 사이트, "자원 소유자"승인
  • 그들이 방문을 승인하면 Tumblr는 그들을 우리의 리셋 URL->callback receives "verifier"
  • 으로 리셋

  • 임시 자격 증명이 있는 GET https://www.tumblr.com/oauth/access_token->사용자 자격 증명(토큰 및 기밀)을 받는
  • 먼저 CLI 클라이언트의 변경 사항을 검토한 다음 AWS Lambda in Rust again을 작성한 이유이기 때문에 리콜을 처리합니다.최종적으로 생성된 사용자 영패/비밀번호는 중복 사용이 가능하기 때문에 고맙게도 계정마다 한 번만 실행할 수 있습니다.

    손님: 네.


    저는 처음에 percent-encoding을 사용하여 문자열 인코딩을 했고 hmacsha-1을 사용하여 필요한 서명을 했고 OAuth 1.0a의 피비린내 나는 디테일을 실현했습니다.하지만 결국 oauth1-request을 선택했습니다. 고객들이 훨씬 간단해졌기 때문입니다.
    먼저 클라이언트/소비자 자격 증명을 사용하여 임시 자격 증명을 얻습니다.
    let uri = format!("{}/oauth/request_token", WWW);
    let client_credentials =
        oauth1_request::Credentials::new(&self.consumer_key, &self.consumer_secret);
    // Sign request using only client/consumer credentials
    let auth_header =
        oauth1_request::Builder::<_, _>::new(client_credentials, oauth1_request::HmacSha1)
            // Can optionally specify callback URL here, otherwise Tumblr will use the application default
            //.callback("https://callback_url")
            .post(uri.clone(), &());
    let resp = self
        .client
        .post(uri)
        .header(reqwest::header::AUTHORIZATION, auth_header)
        .send()
        .await?;
    
    let resp_body = resp.text().await?;
    // Parse `key0=value0&key1=value1&...` in response body for temporary credentials
    let mut resp_body_pairs = resp_body
        .split('&')
        .map(|pair| pair.split_once('='))
        .flatten();
    let temp_token = get_value(&mut resp_body_pairs, "oauth_token")?.to_owned();
    let temp_token_secret = get_value(&mut resp_body_pairs, "oauth_token_secret")?.to_owned();
    
    현재 우리는 임시 증서가 생겼다.우리는 여전히 oauth_verifier을 우리의 리셋 URL에 보내야 한다.이 정보는 다이얼 백 AWS Simple Queue Service (SQS)을 사용하여 수신합니다.
    Rust의 AWS와 상호작용을 하려면 recently announced 공식 AWS SDK for Rust에 AWS 방문 증빙서류가 필요합니다.AWS 액세스 자격 증명을 얻으려면 AWS 콘솔에서 $YOUR NAME(오른쪽) > 내 보안 자격 증명 > 액세스 키 > 새 액세스 키를 생성합니다(이러한 보안 유지).다음 environment variables을 설정합니다.
    $Env:AWS_ACCESS_KEY_ID="xxx"
    $Env:AWS_SECRET_ACCESS_KEY="yyy"
    $Env:AWS_DEFAULT_REGION="us-east-1"
    
    이제 계속해서 고객:
    // Create temporary SQS queue `bullhorn-{temporary token}` to receive oauth_verifier from lambda
    let queue_name = format!("bullhorn-{}", temp_token);
    let client = aws_sqs::Client::from_env();
    let output = client.create_queue().queue_name(queue_name).send().await?;
    let queue_url = output.queue_url.unwrap();
    
    // Show "resource owner" approval website in system default web browser
    let query = format!("{}/oauth/authorize?oauth_token={}", WWW, temp_token);
    let exit_status = open::that(query)?;
    
    "open" crate은 브라우저에 Tumblr 자원 소유자 승인 페이지를 쉽게 표시할 수 있습니다.사용자가 Tumblr 응용 프로그램을 승인할 때, 우리는 SQS를 통해 oauth 인증서를 보낼 때까지 기다립니다.
    // Receive oauth_verifier from lambda via SQS
    let messages = loop {
        let output = client
            .receive_message()
            .queue_url(&queue_url)
            .send()
            .await?;
        if let Some(msgs) = output.messages {
            if !msgs.is_empty() {
                break msgs;
            }
        }
    };
    // Delete the temporary SQS queue
    let _ = client.delete_queue().queue_url(queue_url).send().await?;
    let verifier = messages[0].body.as_ref().unwrap();
    
    // Exchange client/consumer and temporary credentials for user credentials
    let uri = format!("{}/oauth/access_token", WWW);
    let temp_credentials = oauth1_request::Credentials::new(&temp_token, &temp_token_secret);
    // Must authenticate with both client and temporary credentials
    let token = oauth1_request::Token::new(client_credentials, temp_credentials);
    let auth_header =
        oauth1_request::Builder::<_, _>::with_token(token, oauth1_request::HmacSha1)
            // Must include `oauth_verifier`
            .verifier(verifier.as_ref())
            .get(uri.clone(), &());
    let resp = self.client
        .get(uri)
        .header(reqwest::header::AUTHORIZATION, auth_header)
        .send()
        .await?;
    
    우리가 받은 사용자 영패와 비밀번호는 취소되기 전에 유효하기 때문에 (사용자 설정 > 응용 프로그램을 통해) 다시 그러지 않도록 안전한 곳에 저장할 수 있습니다.이제 Tumblr를 사용자를 대신하여 사용할 수 있습니다.

    반송


    상술한 내용은 고객의 중요한 부분을 포괄한다.OAuth 인증의 나머지 부분은 Tumblr의 자원 소유자가 웹 페이지의 리셋을 승인한 것입니다.우리는 HTTP 서버를 계속 실행할 수 있지만, 우리가 가끔 필요로 하는 것들에게는 좀 어리석다.이는'서버 없음'과 AWS 람다의 작업으로 보인다.

    기본 Lambda


    As before, 우리는 AWS Lambda Rust runtime을 사용하여 lambda 함수를 실현할 것이다.MUSLfully static Rust binary을 생성하는 데 사용됩니다.Cargo.toml:
    # Rename binary for AWS Lambda custom runtime:
    # https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html
    [[bin]]
    name = "bootstrap"
    path = "src/main.rs"
    
    [dependencies]
    anyhow = "1.0"
    aws_sqs = { git = "https://github.com/awslabs/aws-sdk-rust", tag = "v0.0.10-alpha", package = "aws-sdk-sqs" }
    futures = "0.3"
    lambda_runtime = "0.3"
    serde = { version = "1.0", features = ["derive"] }
    serde_json = "1.0"
    tokio = { version = "1.5.0", features = ["full"] }
    tracing = "0.1"
    tracing-subscriber = "0.2"
    
    .cargo/config:
    [build]
    # Override default build target
    target = "x86_64-unknown-linux-musl"
    
    # Needed on Mac otherwise build fails with `ld: unknown option: --eh-frame-hdr`
    [target.x86_64-unknown-linux-musl]
    linker = "x86_64-linux-musl-gcc"
    
    먼저 src/main.rsbasic example과 유사한 내용을 추가합니다.
    use serde::{Deserialize, Serialize};
    
    #[derive(Deserialize)]
    struct Request {
        oauth_token: String,
        oauth_verifier: String,
    }
    
    #[derive(Serialize)]
    struct Response {
        msg: String,
    }
    
    #[tokio::main]
    async fn main() -> Result<(), lambda_runtime::Error> {
        let func = lambda_runtime::handler_fn(handler);
        lambda_runtime::run(func).await
    }
    
    async fn handler(event: Request, ctx: lambda_runtime::Context) -> anyhow::Result<Response> {
        Ok(Response{ msg: "ok".to_owned() })
    }
    
    이 lambda를 컴파일하기 위해서, 우리는 musl을 사용하여 OpenSSL을 구축해야 한다. (this post 기반)
    ### Install pre-requisites
    # For Mac/OSX: install command line tools and cross-compiler
    xcode-select --install
    brew install FiloSottile/musl-cross/musl-cross
    
    rustup target add x86_64-unknown-linux-musl
    
    ### Build OpenSSL with musl
    wget https://github.com/openssl/openssl/archive/OpenSSL_1_1_1f.tar.gz
    tar xzf OpenSSL_1_1_1f.tar.gz
    cd openssl-OpenSSL_1_1_1f
    export CROSS_COMPILE=x86_64-linux-musl-
    # `-DOPENSSL_NO_SECURE_MEMORY` is to avoid `define OPENSSL_SECURE_MEMORY` which needs `#include <linux/mman.h>` (which OSX doesn't have).
    ./Configure CFLAGS="-DOPENSSL_NO_SECURE_MEMORY -fpie -pie" no-shared no-async --prefix=output_abs_path/musl --openssldir=output_abs_path/musl/ssl linux-x86_64
    make depend
    # Use `sysctl -n hw.physicalcpu` or `hw.logicalcpu` with `-j` if so inclined
    make
    # Install required stuff to `output_abs_path/musl/` (exclude man-pages, etc.)
    make install_sw
    # Set value from `--prefix` above
    export OPENSSL_DIR=output_abs_path/musl
    
    ### Build Rust lambda function
    cargo build --release
    
    현재 우리는 AWS 콘솔을 사용하여 lambda를 만들 수 있지만, AWS lambda rust가 실행될 때 문서는 AWS CLI을 통해 이를 실현하는 방법을 보여 준다.
    # Set AWS credentials: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
    aws configure
    
    # Create package with layout required for custom lambda runtime:
    # https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html
    # -j/--junk-paths avoids directory structure
    zip --junk-paths lambda.zip ../target/x86_64-unknown-linux-musl/release/bootstrap
    
    # Get ARN for desired existing role
    aws iam list-roles
    
    # Create new lambda function
    aws lambda create-function \
        --function-name bullhorn \
        --handler doesnt.matter \
        --zip-file fileb://./lambda.zip \
        --runtime provided \
        --role arn:aws:iam::01234:role/service-role/bullhorn \
        --tracing-config Mode=Active \
        # Needed with CLI V2 
        --cli-binary-format raw-in-base64-out
        --cli-connect-timeout 10000
    
    # Run the lambda
    aws lambda invoke \
        --function-name bullhorn \
        --payload '{"oauth_token": "xxx", "oauth_verifier": "yyy"}' \
        # Needed with CLI V2
        --cli-binary-format raw-in-base64-out \
        response.json
    
    # To update lambda binary
    aws lambda update-function-code \
        --function-name bullhorn \
        --zip-file fileb://./lambda.zip \
        --cli-connect-timeout 10000
    
  • --cli-connect-timeout 처리 Error: Connection was closed before we received a valid response (issue 참조)
  • 실패한 명령
  • 기본값이 --region <region> 이외의 영역에 aws configure 추가
  • API 게이트웨이


    Tumblr가 리셋 URL을 통해 lambda를 호출하도록 하기 위해서, 우리는 AWS API Gateway을 사용하여 HTTP 포트를 통해 lambda를 터치합니다.
    AWS 콘솔에서 lambda를 열고 + 트리거 추가 > API 게이트웨이 > API를 생성합니다. "API 형식"REST API ("HTTP"도 적용될 수 있습니다) 와 "안전성"을 열 수 있습니다.
    트리거 목록에서 세부 정보를 확장하고 API 끝점을 확인합니다.이것은 Tumblr 응용 프로그램의 기본 콜백 URL과 클라이언트의 oauth_callback 매개 변수로 설정할 수 있습니다.http://tumblr/authorize이 리셋으로 바뀌면 https://your_callback_url?oauth_token=xxx&oauth_verifier=yyy을 받을 수 있습니다.분명히 lambda는 검색 문자열(?xxx=yyy비트)을 받지 않기 때문에 move the query into the body이 필요합니다.
    AWS 콘솔 open API Gateway and/bullhorn ANY > 통합 요청에서 Lambda 프록시 통합 사용을 취소하고 맵 템플릿을 확장합니다.정의된 템플릿이 없는 경우 맵 템플릿 추가 입력 application/json 및 클릭☑. 이제 두 가지 옵션이 있습니다.

  • 템플릿 생성에서 메서드를 선택하여 전송을 요청합니다.이렇게 하면 lambda의 입력에 전체 요청이 포함됩니다.
    transformations: {
    "body-json" : {},
    "params" : {
        "path" : {}
        ,"querystring" : {
            "oauth_verifier" : "yyy",
            "oauth_token" : "xxx"
        }
        ,"header" : {
        }
    },
    "stage-variables" : {
    },
    "context" : {
        ...
    

  • 질의 매개 변수를 요청 본문으로 이동하는 간단한 매핑을 만듭니다.
    {
        "oauth_token": "$input.params('oauth_token')",
        "oauth_verifier": "$input.params('oauth_verifier')"
    }
    
  • 후자는 더욱 쉽다.자세한 내용은 docs for template syntax 을 참조하십시오.
    저금하는 것을 잊지 마라!
    API 게이트웨이에서 메소드 GET 및 조회 문자열 oauth_token=xxx&oauth_verifier=yyy의 작동 상태를 확인하고 테스트할 수 있습니다.로그 출력에서 원시 검색, 비추는 단점 요청 주체와 lambda의 출력을 볼 수 있습니다.
    작업이 정상적으로 수행되면 작업 > 배포 API를 선택하고 배포 단계를 기본값으로 설정한 다음 배포해야 합니다.그렇지 않으면 lambda는 초기 API 게이트웨이 구성에서 질의 문자열을 수신합니다.
    기본적으로 API 게이트웨이에는 CloudWatch 로그가 설정되어 있지 않습니다.To enable logging 캐릭터를 만들고 API Gateway > {select API} > Stages > default > Logs/Tracing에서 사용해야 합니다.

    마지막 람다.


    SQS 큐를 사용하려면 다음과 같은 추가 권한이 필요합니다.
    AWS 콘솔의
  • 에서 IAM
  • 열기
  • lambda 함수에 사용할 캐릭터를 선택하고 전략을 전개합니다

  • 정책 편집 > 추가 권한 추가
  • "서비스"= SQS
  • 작업 = 읽기/GetQueueUrl 및 쓰기/SendMessage
  • "자원"= arn:aws:sqs:us-east-1:01234:bullhorn-*(우리 SQS 대기열에서 사용하는 이름 규약)
  • lambda의 작업 버전은 SQS의 전송 부분일 뿐입니다.
    async fn handler(event: Request, _ctx: lambda_runtime::Context) -> anyhow::Result<Response> {
        let client = aws_sqs::Client::from_env();
        // Get SQS queue based on name set by client
        let queue = client
            .get_queue_url()
            .queue_name(format!("bullhorn-{}", event.oauth_token))
            .send()
            .await?;
    
        // Send oauth_verifier to client so it can retrieve user token/secret
        let res = client
            .send_message()
            .message_body(event.oauth_verifier)
            .set_queue_url(queue.queue_url)
            .send()
            .await?;
        let response = Response {
            message_id: res.message_id,
            sequence_number: res.sequence_number,
        };
        Ok(response)
    }
    
    Dell client은 검증기를 수신하고 앞서 설명한 OAuth 인증을 완료합니다.
    SQS와 같은 위탁 관리 서비스는 보통 eventual consistency으로 표시되기 때문에 get_queue_url()이 일시적으로 실패하는지 알고 싶습니다. 다시 시도해야 합니다.지금까지 작업 방식이 좋았기 때문에 클라이언트가 대기열을 만들 때 고려해야 할 문제일 수도 있습니다.

    Tumblr에 게시


    지금까지 모든 것은 OAuth와 사용자 자격 증명 취득과 관련이 있습니다.일단 우리가 그것들을 가지게 되면, 우리는 Tumblr와 상호작용할 수 있다.
    텀블러 API에는'레거시'형식도 있고'neue'형식도 있다.Legacy는 가격 인하를 지원하지만 사양 URL은 지원하지 않습니다.Neue는 규범화된 URL이 있지만 가격을 내리지 않았습니다.직위는 반드시 Neue Post Format (NPF) blocks명으로 구성되어야 한다.태그를 HTML로 만들어 블록에 삽입하는 것이 유용할 수 있지만 나중에 검토해 보겠습니다.더 좋은 해결 방안을 찾기 전에, 우리는 원본 글의 하이퍼링크일 뿐, '링크' 게시물을 만들 것입니다.
    // Check if the article already exists
    let posts: Posts = self
        .client
        .get(format!("{}/blog/{}/posts", URL, self.blog_id))
        // Only requires api_key authentication, get response in "Neue Post Format"
        .query(&[("api_key", &self.consumer_key), ("npf", &"true".to_owned())])
        .send()
        .await?
        .json()
        .await?;
    let existing = posts.response.posts.iter().find_map(|p| {
        p.content
            .iter()
            // Find block that is a "link" and contains canonical URL, and return its ID
            .find(|block| match block {
                ContentBlock::Link { display_url, .. } => {
                    display_url == canonical_url
                }
                _ => false,
            })
            .map(|_| p.id_string.clone())
    });
    
    // There's a series of structs for serde to parse the JSON response.
    // The following are the important ones:
    
    #[derive(Debug, serde::Deserialize)]
    struct Post {
        id_string: String,
        content: Vec<ContentBlock>,
        // <SNIP>
    }
    
    // Serde will serialize these from JSON like, e.g.:
    // {type="link", display_url="xxx", ...}
    #[derive(Debug, serde::Deserialize)]
    #[serde(tag = "type", rename_all = "snake_case")]
    enum ContentBlock {
        Link { display_url: String, title: String },
    }
    
    기사 작성/업데이트:
    // Must authenticate using both client/consumer and user tokens/secrets
    let token = oauth1_request::Token::from_parts(
        &self.consumer_key,
        &self.consumer_secret,
        &self.token,
        &self.token_secret,
    );
    let tags = post.front_matter.tags.map(|tags| RequestTags { tags });
    let request = LinkRequest {
        // If we found existing article this will be Some and we'll update.  Otherwise this is None and we create.
        id: existing.clone(),
        title: Some(post.front_matter.title),
        url: canonical_url,
        tags,
        ..Default::default()
    };
    
    // To create an article: POST {blog_id}/post
    // To update: POST {blog_id}/post/edit
    let uri = format!(
        "{}/blog/{}/post{}",
        URL,
        self.blog_id,
        if existing.is_some() { "/edit" } else { "" }
    );
    // Sign the request and create `Authorization` HTTP header
    let auth_header =
        oauth1_request::post(uri.clone(), &request, &token, oauth1_request::HmacSha1);
    // For POST, request body contains `application/x-www-form-urlencoded`
    let body = oauth1_request::to_form_urlencoded(&request);
    
    let resp = self
        .client
        .post(uri)
        .header(reqwest::header::AUTHORIZATION, auth_header)
        .header(
            reqwest::header::CONTENT_TYPE,
            "application/x-www-form-urlencoded",
        )
        .body(body)
        .send()
        .await?;
    
    // HTTP request to create/update "link" type post
    #[derive(oauth1_request::Request)]
    struct LinkRequest {
        /// Must be `Some` when updating an existing article, `None` when creating a new one
        id: Option<String>,
        #[oauth1(rename = "type")]
        r#type: String,
        tags: Option<RequestTags>,
    
        // <SNIP>
    }
    
    // Helper to serialize Vec<_>
    struct RequestTags {
        tags: Vec<String>,
    }
    
    // Need to impl Display so oauth1_request knows how to serialize a Vec<_>
    impl std::fmt::Display for RequestTags {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
            let tag_param = self.tags.join(",");
            write!(f, "{}", tag_param)
        }
    }
    
    이로 인해 발생한 게시물은 다음과 같습니다.

    좋은 웹페이지 즐겨찾기