コロナウイルス対応のため、学習コンテンツの無料開放にひと肌脱いだ話。

##０．まえおき

新型コロナウイルスによる、学校の休校を受け、
教育業界において、コンテンツの無料開放などが流行っています。

そんなさなか、とある老舗の参考書出版会社さまから、
コンテンツ無料解放をしたいと相談を受けたのが金曜日の夜。。。

すばらしい社会貢献！
ひと肌脱ぐことになり、私の休日(ほぼ徹夜)は消え去りました。
(えぇ、もちろん私も無償奉仕です。)

<img width="600px" alt="CORS" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/05d066bf-c097-16b8-49ab-1b254e1d8ad3.png">

##１．要件/状況
**・PDFの書籍データがある**
**・誰にでも見せたいけど、ダウンロードとか印刷をされてしまっては困る。**
　（過度には誰にも見せたくない、、、）
**・サーバとか何もない**

という、本当にゼロからのスタートです；；

<br>
<br>
<br>

##２．実現方法概要
**１．PDF(URL)の保護**
　PDF自体のDRMとかもあるのかもしれないですが、Adobeが絡むはずなので、(あまり詳しくないです)
　AWSの署名付きURLで期限付きのURLを発行して、URLの流出やクローラに対応。

**２．PDFのDLや印刷を禁止する**
　ブラウザでのPDF閲覧ではなく、独自Viewer(PDF.js)を採用し、DF閲覧時の機能を制限する。

**３．時間もないためにサーバレスで実装**
　静的コンテンツであれば、関連するシステム上に配置可能とのことで、
　それ以外については、AWSを多用しまくります。

**４．致し方ないこと**
　・それなりにITの知識がある人にはどうしてもPDFデータはDLされちゃう。
　・画面キャプチャなど、OSによる機能は防げない。

<br>
<br>
<br>

##３．という事で、全体のシーケンス
<img width="800px" alt="全体概要" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/3685cee7-b002-8871-e42d-9e00dc9801c6.png">

もっと、いい案がないのか、、、と思いつつも時間がないので、即実装です。
　

**①AWS API Gateway**
　・GETリクエストで、閲覧したい書籍のパラメータを受ける。
　・APIについては、IP制限が使えないため(IP固定なのか不明・・・)、アクセス元ドメインでのリファラー制限を実施。
　　   (リファラー詐称をされない限り、APIは実行不可)


**③AWS Lamda**
　・受け取った書籍情報について、S3上のPDFに対し署名付きURLを発行する。
　・そのままURLを返却してしまうと通常のブラウザで見えてしまうので、
　　署名付きURLを付与したViewerのURLへリダイレクトさせる。

**③PDF.js**
　・パラメータにある署名付きURLのデータをPDF.jsが読み込む。
　　（この際、PDF.jsのパラメータを見て、ヨシヨシ！という感の鋭い人には、PDFそのもののアクセスができちゃう。
　　　ただ、URLがすごく長いのでぱっと見わからないかも。）

**④公開領域フォルダ**
　・DLや印刷機能を禁止したカスタマイズ版のPDF.jsを配置します。

**⑤PDFの書籍データ**
　・署名付きURLでしかアクセスできないようにします。
　・Viewerからアクセスされるので特定ドメインのCORS設定も行います。

<br>
<br>
<br>

##４．各々の詳細
###①②AWSのLambdaコードです。(node.js v12.Xです)

ポイントは、Lambda プロキシ統合利用でのCORS設定と、
302リダイレクトのあたりでしょうか。

APIGatewayとLambdaについては、前回の記事も参考ください。
[AWS APIGateway + Lambda プロキシ統合　の利用でCORS設定にはまった話](https://qiita.com/maechi/items/069d37dc5599de013508)


```nodejs:署名付きURLの発行と302リダイレクト
const https = require('https');

const AWS_ACCESSKEY_ID =  process.env.AWS_ACCESSKEY_ID;
const AWS_SECRET_ACCESSKEY =  process.env.AWS_SECRET_ACCESSKEY;
const PDF_BASE_URL = process.env.PDF_BASE_URL;
const PDF_BUCKET = process.env.PDF_BUCKET;
const PDF_EXPIRES = parseInt(process.env.PDF_EXPIRES,10);

let AWS = require('aws-sdk');
AWS.config.update({accessKeyId: AWS_ACCESSKEY_ID, secretAccessKey: AWS_SECRET_ACCESSKEY,region:"ap-northeast-1"});
let s3 = new AWS.S3({signatureVersion:'v4'});


exports.handler = async function (event, context, callback) {

    const done = (err, res) => callback(null, {
        statusCode: err ? '400' : '302',
        body: res,
        headers: {
            "Access-Control-Allow-Origin": "*",　//Lambdaプロキシ統合の場合は自前でCORS設定。
            "Location": res //302リダイレクトのために独自設定
        }
    });

    switch (event.httpMethod) {
        case 'GET':

            const book = event['queryStringParameters']['book'];
            const page = event['queryStringParameters']['page'];
            
            let presignedURL = await createPresignedURL(PDF_BUCKET,book+"/"+page+".pdf");
            let return_url = PDF_BASE_URL+"?file="+encodeURIComponent(presignedURL);
            console.log('createPresignedURL :' + return_url);  
            
            done(null,return_url);
        
            break;
        default:
            done(new Error(`Unsupported method "${event.httpMethod}"`));
    }
    
};


/**
 * 対象のURLに対して署名を行い、公開用のURLを発行します。
 */
async function createPresignedURL(inBucket,pdffilename){
    
    const s3params = {
        Bucket: inBucket,
        Key: pdffilename,
        Expires: PDF_EXPIRES
    };
    console.log('createPresignedURL:', JSON.stringify(s3params));

    try {
        let signed_url = await s3.getSignedUrl('getObject', s3params);
        return signed_url;
    } catch (err) {
        console.log("Err:"+err);
        return err;
    }

}
```

また、API GateWay側にはリダイレクト設定が必要です。
<img width="600px" alt="APIGW" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/561efd86-3f13-62ba-af96-6be22e8cd347.png">


あとは、APIGateWayのリソースポリシー設定を行います。
APIGateway全般なんですが、設定などを変えた後、**「APIのデプロイ」**を忘れがちです。。。 
<img width="800px" alt="APIGW2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/ddee818e-9a8a-1216-1d2e-f1bcae52837c.png">

```json:リソースポリシー
{
    "Version": "2012-10-17",
    "Id": "policy example",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:Hogehoge/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "https://foobar/*"
                    ]
                }
            }
        }
    ]
}
```
<br>
<br>

###③PDF.js(pdfjs-2.2.228)の修正
・PDF.jsについては、DLボタンの禁止
・印刷の禁止
<img width="500px" alt="印刷禁止" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/b80ccb58-e66d-210e-2c7c-997e5e2f660b.png">

・縦スクロールを見やすい横スクロールに
・HOSTED_VIEWER_ORIGINSの設定
などを実施しています。

```css:viewer.css
 /* ツールバーのボタン類を削除 */
#sidebarToggle,
#openFile, 
#viewBookmark,
#print,
#download,
#secondaryOpenFile, 
#secondaryViewBookmark,
#secondaryDownload,
#secondaryPrint,
#secondaryToolbarToggle { 
    display: none !important;
}

 /* ブラウザ印刷時のデータ削除 */
@media print{
  body{
    display: none;
  }
}
```

```nodejs:viewer.js(13900行目あたり)
function getDefaultPreferences() {
  if (!defaultPreferences) {
    defaultPreferences = Promise.resolve({
      "cursorToolOnLoad": 0,
      "defaultZoomValue": "",
      "disablePageLabels": false,
      "enablePrintAutoRotate": false,
      "enableWebGL": false,
      "eventBusDispatchToDOM": false,
      "externalLinkTarget": 0,
      "historyUpdateUrl": false,
      "pdfBugEnabled": false,
      "renderer": "canvas",
      "renderInteractiveForms": false,
      "sidebarViewOnLoad": -1,
      "scrollModeOnLoad": 1, //ここのデフォルトを-1⇒１に変更
      "spreadModeOnLoad": -1,
      "textLayerMode": 1,
      "useOnlyCssZoom": false,
      "viewOnLoad": 0,
      "disableAutoFetch": false,
      "disableFontFace": false,
      "disableRange": false,
      "disableStream": false
    });
  }
```
<br>
<br>

###④公開領域バケットの設定
（割愛）
公開領域といえども、無駄なアクセスを避けるため、
気持ち程度にリファラー設定。

<img width="800px" alt="APIGW" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/5adb00cf-3d99-2256-5ad8-cf2925e383db.png">

```json:バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "policyexample",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::hogehoge/*",
            "Condition": {
                "StringLike": {
                    "aws:Referer": [
                        "https://foo.bar/*"
                    ]
                }
            }
        }
    ]
}
```

<br>
<br>

###⑤非公開領域バケットの設定

（割愛）
Viewerから呼び出されるために、CORS設定が必要になります。
<img width="800px" alt="CORS" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93661/a3063323-6e0c-64c1-518d-7979fbf56294.png">

```xml:CORSの設定
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://hogehohogehoge</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <ExposeHeader>ETag</ExposeHeader>
    <ExposeHeader>Accept-Ranges</ExposeHeader>
    <ExposeHeader>Content-Encoding</ExposeHeader>
    <ExposeHeader>Content-Length</ExposeHeader>
    <ExposeHeader>Content-Range</ExposeHeader>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```
<br>
<br>
<br>

##５．あとがき
サービスサイトから、GETパラメータを渡すことで、無事にPDFの表示ができています。
不用意にコピーされたくない、PDFをWeb上で公開する方法の１案ではないでしょうか。

作成しましたサイトですが、
公開可能でしたら、後日ご紹介したいと思います。 
（フロント側については、もう一人のエンジニアの方と実装&ブラッシュアップ中です）


**なんだかんだ、時間を要したのは、PDFの加工(原本はGB単位)だったりしました。。。**
（PDFを命名ルールを付けながらリネーム、分割、圧縮、結合、ディレクトリへの配置。）
<br>