素人エンジニアの開発日記

個人開発が趣味の素人エンジニアが日記を書きます

React+canvasで別ドメインの画像ファイルをローカルに保存する

Reactでcanvasを使用して「ボタンを押したら画像をダウンロードする機能」を実装しようとしたら詰まったので備忘録 かなりニッチなニーズ

やりたいこと

ボタンのクリックで画像をダウンロードする機能を作ります 別ドメインだと厄介だったので残しておきます

1.gif

aタグによる画像のダウンロード

通常aタグにdownload属性をつければファイルは簡単にダウンロードできますが

<a href="https://hoge.com/sample.png" download="saved.png">ダウンロード</a>

のように別ドメインのサーバーに存在する画像を指定すると別タブで開くだけです

aタグのdownload属性は同一オリジンでのみ動作するので別ドメインの画像はダウンロードできません

canvasを使用してダウンロード

canvasを使用して画像をblobに変換してダウンロードする方法

    const c = document.getElementById('canvas');
    c.toBlob((blob) => {
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        document.body.appendChild(a);
        a.download = 'foo.png';
        a.href = url;
        a.click();
    }, 'image/png');

こんな感じでblobに変換してダウンロードすることが可能なのでこちらを試してみます

しかし、、

Tainted canvases may not be exported

こんな感じのエラーが発生

キャンバスは別オリジンの画像を入れると汚染されてしまいtoBlobなどのメソッドが使えなくなるそう

crossOrigin="anonymous"

canvas内のimgタグに crossOrigin="anonymous" をつけると別オリジン間でのダウンロードが可能になります

余計な画像のキャッシュ

crossOrigin="anonymous" をつけたらダウンロードできるようになる場合とならない場合が

前提としてS3に画像を保存している場合、S3は Orgin ヘッダが含まれていないと Access-Control- 系のヘッダを返しません

crossOrigin属性を付ける前のimageタグではOriginヘッダを送信せずにオリジン許可が得られません その画像のキャッシュを保存している可能性があり怒られます

のでキャッシュを消してあげましょう

<img src="https://hoge.com/sample.png?cache=none" crossOrigin="anonymous" alt=""/>

のようにすればキャッシュの問題も解決できる S3の場合はこれでキャッシュの削除ができたが他のサーバーではできないこともあったので他の方法を検討したほうが良さそう

Reactで画像ダウンロード機能を実装してみる

今回は2枚の画像を同時にダウンロードする機能を作成します

まずはカスタムフック

import { useRef, RefObject } from 'react';

const useEnhancer = () => {
  const canv = useRef<HTMLCanvasElement>(null);
  const canv2 = useRef<HTMLCanvasElement>(null);
  const img = useRef<HTMLImageElement>(null);
  const img2 = useRef<HTMLImageElement>(null);

  const downloadImage = (
    canvas: RefObject<HTMLCanvasElement>,
    image: RefObject<HTMLImageElement>,
    id: string
  ) => {
    if (canvas.current !== null) {
      const currentCnavas = canvas.current;
      const ctx = currentCnavas.getContext('2d');
      if (ctx && image.current !== null) {
        const currentImage = image.current;
        currentCnavas.width = currentImage.width;
        currentCnavas.height = currentImage.height;
        ctx.drawImage(currentImage, 0, 0, currentImage.width, currentImage.height);
      }
      const anchor: HTMLAnchorElement = document.createElement('a');
      currentCnavas.toBlob((blob: any) => {
        if (anchor !== null && blob) {
          anchor.href = window.URL.createObjectURL(blob);
          anchor.download = `${id}.png`;
          anchor.click();
        }
      });
    }
  };

  return {
    downloadImage,
    canv,
    canv2,
    img,
    img2,
  };
};

export default useEnhancer;

View

import React from 'react';
import useEnhancer from './enhance';

const DownloadImage = () => {
  const enhance = useEnhancer();

  return (
    <div>
      <button
        onClick={() => {
          enhance.downloadImage(enhance.canv, enhance.img, '1');
          enhance.downloadImage(enhance.canv2, enhance.img2, '2');
        }}
        type="button"
      >
        ダウンロード
      </button>
      <canvas ref={enhance.canv} style={{ display: 'none' }}>
        <img
          ref={enhance.img}
          src="https://cdn.qiita.com/assets/qiita-fb-fe28c64039d925349e620ba55091e078.png?cache=none"
          alt=""
          crossOrigin="anonymous"
        />
      </canvas>
      <canvas ref={enhance.canv2} style={{ display: 'none' }}>
        <img
          ref={enhance.img2}
          src="https://cdn.qiita.com/assets/qiita-fb-2887e7b4aad86fd8c25cea84846f2236.png?cache=none"
          alt=""
          crossOrigin="anonymous"
        />
      </canvas>
    </div>
  );
};

これにて一件落着

参考

Google Chromeでのamazon S3画像へのクロスドメイン接続: stackoverflow

ポート開放してたらDB破壊され身代金を請求されかけた話

CA Tech Dojo/Challenge/JOB Advent Calendar 2019 の5日目のエントリーです。

この記事はツッコミどころ満載です。どうぞご自由にツッコんでくださいませ。

大学院休学中の怠け野郎です。

mooriii.com

11/7~11/29にAbemaTVのフロントエンドでCA Tech JOBに参加しました。

インターンでは新規ページの仕様策定〜実装までやらせてもらいました。

ディレクターの方やデザイナーの方と連携しながら仕事をさせてもらいとてもいい経験になりました。

今日は油断してたら自作webサービスのDBが破壊され身代金を請求されかけた話をします。

概要

レンタルVPSのDBポートを意図的に開放&外部接続可能にした結果、0.2ビットコインを請求されかけました。

事の発端

僕はYYさんという実況者がめちゃめちゃ好きで毎日見ています。

www.youtube.com

YYさんの好きなシーンはたくさんあって時々「あのシーンみたいなぁ」ってことがあるのですが、どの動画のどのシーンかわからなくて探すのに時間をかけたり、結局見つからなかったりしてました。

そこで、「みんなの好きなシーン集めちゃえばいいんじゃね?」と思い立ち、YYさんの好きなシーンを投稿するSNSを作成しました。

ytube-938fd.firebaseapp.com

サイト紹介

システム概要

バックエンドはRails、フロントはReact×Ts、認証にはFirebase Authenticationを使用しています。YYさん以外の動画を弾くためにバックエンドでYoutube Data APIも使用しています。サーバーはさくらのVPSです。

認証はこんな感じ。

f:id:mr04vv:20191201133440p:plain
認証システム構成

機能紹介

ホーム画面にある+ボタンを押すと投稿画面が表示されます。投稿したい動画のURLをコピペして、好きなシーンの開始地点と終了地点でボタンを押して投稿します。

f:id:mr04vv:20191201133837p:plain
ホーム画面

f:id:mr04vv:20191201134106p:plain
投稿画面

投稿するとホーム画面に追加され、好きなシーンの開始地点から再生が始まり終了地点まで行くと、開始地点に戻り再び再生が始まります。

簡単な検索も可能になってます。

f:id:mr04vv:20191201134423p:plain
検索画面

本題

サイトの話はこの辺にしておいて、本題に入ります。

「本番DBが見たい」

これが悪夢の始まりでした。

僕はSQLGUIクライアントにSequel Proを使っています。

www.sequelpro.com

Sequel Proから本番環境に接続するためには外部接続の許可&ポート開放が必要なので、「まあ接続確認だけだし、そんな簡単にハックされることはないだろ。すぐ戻すから大丈夫」と思い普通に

mysql> GRANT ALL PRIVILEGES ON *.* TO root@'%' IDENTIFIED BY 'root' WITH GRANT OPTION; *1

*1: コマンドはイメージです

自分のPCはあらゆるWiFiに接続するのでIP指定することもできず仕方なく全開放しました。

無事Sequel Proから本番DBが見えるようになりました!

数日後、、

「あれ、APIからレスポンスが返ってこない」

「どうせnginxの不調やらRailsサーバーが落ちたとかそういう類でしょ。」と思いながらSequel Proに接続。

すると、、、

f:id:mr04vv:20191201163034p:plain

もともとあったデータベースが削除されWARNINGというデータベースが作成されていました。

f:id:mr04vv:20191201163153p:plain

その中にはPLEASE_READという謎のテーブルがただ存在しているだけでした。

「怖」

もうその一言に尽きます。

速攻で検索。(いや、先に権限直せ)

するとこんな記事が。

www.guardicore.com

要約すると、

「お前のDBを乗っ取った。DBのダンプがほしけりゃ金を払え。」

というランサムウェア攻撃。

幸いサイトは開発段階でDBはほぼ空っぽだったので被害はなし。危な。

なんとか事なきを得ました。

まとめ

自作サイトのDBのパスワードをrootのまま外部接続を許可したらデータを消され乗っ取られました。

その後は、rootのパスワードを変え、rootの外部接続を切り、専用のユーザーを作成し権限を与えて解決しました。

今では投稿数は200を超え、ユーザー数は50人を超えるまで増えてくれました。

以上セキュリティは大事だよ。というお話でした。

黙ってポートフォワーディングしとけってことです。

ではまた。

ytube-938fd.firebaseapp.com

CircleCIでS3にデプロイしようとしたらSyntaxError: invalid syntaxで失敗した

いつもlintでエラー出てたので、どうせlintだろと思ってログを見たら

# aws --version
Traceback (most recent call last):
  File "/bin/aws", line 19, in <module>
    import awscli.clidriver
  File "/usr/lib/python2.7/site-packages/awscli/clidriver.py", line 36, in <module>
    from awscli.help import ProviderHelpCommand
  File "/usr/lib/python2.7/site-packages/awscli/help.py", line 20, in <module>
    from docutils.core import publish_string
  File "/usr/lib/python2.7/site-packages/docutils/core.py", line 246
    print('\n::: Runtime settings:', file=self._stderr)
                                         ^
SyntaxError: invalid syntax

??!

なんだと思って調べてみたら、こんなissueを発見。

github.com

pythonのdocutilのバージョンが0.15に上がったことによる不具合らしい

configのaws cliのインストール後に pip install docutils==0.14 をすると治るとか

pip install awscli --user
pip install docutils==0.14

successした。いぇい。

redux-actionsをtypescriptで使いたい人へ

Reactの0→1案件でReduxを導入したんですが Action書くのだるいなぁと思ってたらこんなものを見つけました

github.com

これを使うとactionとreducerが完結にかけるらしい

// Actions
export const isLoading = (): LoginActionType => ({
  type: LOGIN,
  payload: null,
});

const loginSuccess = (result: any): LoginActionType => ({
  type: LOGIN_SUCCESS,
  payload: result,
});

const loginFail = (err: any): LoginActionType => ({
  type: LOGIN_FAIL,
  payload: err,
});

// Reducer
const login = (state = initialState, action: LoginActionType): LoginState => {
  switch (action.type) {
    case LOGIN:
      return {
        ...state,
        loading: true,
      };
    case LOGIN_SUCCESS:
      return {
        ...state,
        data: action.payload,
        loading: false,
        status: 'success',
      };
    case LOGIN_FAIL:
      return {
        ...state,
        data: action.payload,
        loading: false,
        status: 'fail',
      };
    default:
      return state;
  }
};

例えば、こんなactionとreducerが(interfaceとか色々割愛してます)

// Actions
export const loadingAction = createAction(LOGIN);
const loginSuccessAction = createAction(LOGIN_SUCCESS, (res: any) => res);
const loginFailAction = createAction(LOGIN_FAIL, (err: any) => err);

// // Reducer
const login = handleActions({
  [loadingAction.toString()]: state => ({
    ...state,
    loading: true,
  }),
  [loginSuccessAction.toString()]: (state, action) => ({
    ...state,
    data: action.payload,
    loading: false,
    status: 'success',
  }),
  [loginFailAction.toString()]: (state, action) => ({
    ...state,
    data: action.payload,
    loading: false,
    status: 'fail',
  }),
}, initialState);

こんな完結にかけちゃいます

ただ若干ts対応が弱い…

[action.toString()] にしないとエラーが出ます (もしくはactionの返り型をanyにする)

色々調べていると

github.com

このissueのコメント欄にこんなものがありました

Hello all.

I don't think the pattern of redux-actions can give a full type inferring capability. at least not yet. Also, there is no official support for typescript in redux-actions. All of these makes using redux-actions in typescript > hard and misleading.

That's why I have just published thebrodmann/deox as a good alternative to redux-actions for typescript use cases with full power of type inferring without any type information losing. I hope it makes sense.

deox がtypescript使いのredux-actionsの代わりになる」と…

半信半疑で使ってみました

// Actions
export const isLoading = createActionCreator(LOGIN);
const loginSuccess = createActionCreator(LOGIN_SUCCESS, resolve => (res: object) => resolve(res));
const loginFail = createActionCreator(LOGIN_FAIL, resolve => (err: string) => resolve(err));

// Reducer
const login = createReducer(initialState, handleAction => [
  handleAction(isLoading, state => ({
    ...state,
    loading: true,
  })),
  handleAction(loginSuccess, (state, action) => ({
    ...state,
    data: action.payload,
    loading: false,
    status: 'success',
  })),
  handleAction(loginFail, (state, action) => ({
    ...state,
    data: action.payload,
    loading: false,
    status: 'fail',
  })),
]);

記述量はそんな変わりませんね あと変なエラーも出なくなりました

github.com

The only completely functional type-safe approach to Flux that its main goals are to diminish types verbosity, repetition and complexity without losing any type information (type-safe alternative of redux-actions).

型安全な redux-actionsだよ〜って感じですかね

star数こそ劣っているものの、最終更新はdeoxのほうが新しく最近もサポートされているみたいです

  • star
    • redux-actions : 6114
    • deox : 83
  • last commit
    • redux-actions : 4 months ago
    • deox : 4 days ago

割と問題なく使えたのでtsでredux-actions導入しようと思ってる人は使ってみてはいかがでしょう

xip拡張子とは?

iPadのsidecarを使いたいがためにMacをcatalinaにアップデートしたらxcodeの11betaがインストールできるみたいでインストールしようとしたらXcode_11_Beta_2.xip という形式でダウンロードされていました。

xipってなんだ?と思ったので調べてみました。

man xip で見てみると

DESCRIPTION
     The xip tool is used to create a digitally signed archive. As of macOS
     Sierra, only archives that are signed by Apple are trusted, and the for-
     mat is deprecated for third party use.

アーカイブが展開される前にデジタル署名が適用されて検証されるみたいですね。

zipのデジタル署名版ってところでしょうかね?

Type-C一本で電源供給と出力してくれるモニターがやばすぎた

ついに、今日念願のモニターが届きました!

購入したモニターはこちら

 


 

 

最近主流になっているUSB-Cタイプのモニターです

 

僕はMacbookを使っているのですが

今までは電源供給にACアダプター、出力にHDMI、HHKBにUSBを使っていて

www.apple.com

これが必須でした。。

研究室に持っていくのに電源用のACアダプターをいちいち取り外し、持っていかなきゃいけませんでした。


そんな中、このモニターに出会いました。


type-c一本で電源供給とモニター出力をしてしまうのです!

しかもHHKBが直接モニターにさせる!!

最高かよ!!!

 

いままでは↓これでした

f:id:mr04vv:20190619203938j:plain



でも今は

f:id:mr04vv:20190619203934j:plain

これだけです。

 

 

最高にスッキリしました(^^)

Reactのコンポーネント名の先頭は大文字にしろ

完全につまらんミスを犯してしまいました。

コンポーネント名の先頭を小文字にするとhtmlタグと認識されてトランスパイルされてしまうみたいなのでコンポーネント名の先頭は大文字にしましょう。

tslintで関数名は小文字にしろって怒られて変更したらこのザマ。

functional componentは罠なのか…笑

気をつけましょう。笑