otp 구현하기

  • 갑자기 otp를 구현하고 싶어졌다

Unix Epoch Time (Unix Time) 구하기

js
1
new Date().getTime(); // unix time

N 구하기

js
1
2
const ts = 30; // 30초 간격으로 토큰을 생성할 것
let N = Math.floor(new Date().getTime() / (ts * 1000));
  • unix time에서 토큰 생성간격 ts 만큼을 나누고 소수점은 버려서 N을 구한다

N 16진수 변환

js
1
let N_hex = ('000000000000000' + N.toString(16)).substr(-16); // 16자리가 되도록 앞에 0 패딩추가
  • N을 16진수로 변환하는데 16자리가 되도록 처리

m 구하기 (시간 베이스 메세지 값)

js
1
let m = Buffer.from(N_hex);
  • N_hex를 byte array로 변환

K 생성하기 (K : 공유키)

js
1
2
3
4
5
const base32 = require('base32'); // base32 모듈 임포트

const random12 = `${Math.random() * Math.pow(10, 20)}`.substring(0, 12); // 12자리 랜덤숫자 생성

const K = base32.encode(random12);
  • 랜덤값으로부터 20자리의 공유키 K 구하기
  • base32 모듈 설치
  • Math.random()으로 생성한 랜덤값을 12자리 문자열로 변환하고 base32인코딩하면 20자리의 문자열을 얻을 수 있음

HMAC hash 얻기

js
1
2
3
const crypto = require('crypto');

let hmac_hash = crypto.createHmac('sha1', K).update(m).digest('hex');
  • 내장 모듈 crypto 에서 hmac hash를 얻을 수 있다

OTP 생성

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const offset = parseInt(Number(`0x${hmac_hash[hmac_hash.length - 1]}`), 10);

// offset으로부터 4개 바이트 변환
const token_hex_4bytes = hmac_hash.substring(offset * 2, offset * 2 + 4 * 2);
let toekn_hex = '';

toekn_hex += (
'00' + (Number(`0x${token_hex_4bytes.substring(0, 2)}`) & 0x7f).toString(16)
).substr(-2);

for (let index = 2; index < token_hex_4bytes.length; index += 2) {
const element = token_hex_4bytes.substring(index, index + 2);
toekn_hex += ('00' + (Number(`0x${element}`) & 0xff).toString(16)).substr(-2);
}

const token = Number(`0x${toekn_hex}`).toString().substr(-6);
console.log(`token : ${token}`);
  • hmac_hash의 맨 마지막값을 offset으로 한다
  • hmac_hash offset부터 4바이트를 추출한고
  • offset 바이트에 & 0x7f 이진 연산 수행
  • 나머지 3개 바이트에 대해 & 0xFF 연산 수행
  • 6자리 토큰을 만든다고하면 그 결과의 뒤에서 6자리를 토큰으로 한다

개선사항

  • 생성된 토큰 값이 일부분 동일하게 유지된다

참고

nodejs + socketio 로 만드는 채팅 앱, 카카오톡 클론코딩?

  • Web Dev Simplified의 Build a Real Time Chat App With Node.js And Socket.io을 보고 따라만들기
  • nodejs에서 html과 socket.io로 간단하게 챗앱을 만드는 것을 따라해보았다
  • Web Dev Simplified 정말 심플하게 잘알려준다.. ㄷㄷ 따라 만들면서 행복했다
  • 따라만들고 카카오톡같은 css를 입히고,
  • 디테일한 기능을 추가 해줬다

추가한 기능

  • 고유한 프로필 사진 생성(랜덤색 + 이니셜)
  • 현재 채팅에 참가한 유저 목록 위젯
  • width값이 작아지면 유저 목록 위젯 숨김
  • width값이 작아지면 프로필 사진 영역 숨김
  • 같은 사람이 연달아 올릴 때 프로필 사진 생략
  • 같은 사람이 같은 시간에 연달아 올릴 때 가장 최근 메시지에만 시간표시
  • 메시지는 plain text로 표시

소스코드

포트 포워딩 체크 웹 클론코딩, Open Port Check WEB (NodeJs)

  • 서버를 열어놨는데, 외부에서 접근이 가능한지 불가능한지,
  • 포트포워딩을 확인하고 싶으면
  • 핸드폰을 열고 데이터를 켜서 내 서버로 접속해서 확인하는 등 번거롭게 확인했다
  • https://www.yougetsignal.com/tools/open-ports/ 라는 포트포워딩 체크 웹이 있는데
  • 이거를 한번 클론 코딩해보았다

영상설명

  • 나는 ec2에 올려서 테스트해봤다

  • 주소창에 보면 15.165.000.000:4000 으로 나오는데 내 ec2 ip인데 일부러 뒷부분을 000.000으로 바꿔놨다

  • 이 앱은 ec2의 4000포트에서 동작중이다

  • 도메인명이나 ip를 입력하고 포트입력란에 원하는 포트를 입력한다

  • 그리고 check 버튼을 누르면 해당 포트가 열려있는지 확인한다

  • 당연히 해당 포트를 리슨하는 서버가 동작하고 있어야한다

  • 우측 common ports 에서 원하는 포트를 클릭하면 해당 포트로 요청을 보낸다

  • Scan All Common Ports 버튼을 눌러 모든 common port를 스캔한다

  • 0:18 중간에는 마인크래프트 서버를 체크해본다
  • 0:35 localhost로 적으면 내 ec2의 포트를 스캔하게된다
  • localhost를 common port를 모두 스캔해보니 3000번이 열려있다는 것을 알 수 있다
  • 0:48 이 앱이 실행되고 있는 4000번 포트를 스캔해보니
  • 당연하게 열려있는 모습을 확인할 수 있다

소스코드

nodemon 변화감지 디렉터리 지정

cmd
1
nodemon --watch app app/server.js
  • nodemon을 사용해서 코드수정마다 서버를 자동으로 재시작하는데,
  • view와 관련한 코드를 수정할 때 서버가 재시작되는 것은 불필요하고, 세션이 날라가는 등 오히려 불편할 수 있다
  • nodemon이 모든 프로젝트 디렉터리가 아닌 특정 디렉터리의 변화를 감시하고 재시작되는 것을 알아보자

명령어

terminal
1
nodemon ./bin/www
  • 보통 이렇게 아무것도 없이 사용하면, 거의 모든 변화마다 서버가 재시작된다
terminal
1
nodemon --watch routes ./bin/www
  • --watch routes watch옵션을 넣어줌으로써 해당 디렉터리에서 변화가 있으면 재시작하도록 할 수 있다
terminal
1
nodemon --watch routes --watch libs ./bin/www
  • 이런식으로 나열할 수 있다
  • 기본적으로 디렉터리명을 적어야하며, 하위디렉터리는 자동으로 포함된다
  • 그런데 여러개의 디렉터리를 나열을 해야하면은
  • 반대로 무시할 디렉터리를 설정하는게 빠를 수 있다

config file로 설정

nodemon.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"restartable": "rs",
"ignore": [".git", "node_modules/**/node_modules"],
"verbose": true,
"execMap": {
"js": "node --harmony"
},
"events": {
"restart": "osascript -e 'display notification \"App restarted due to:\n'$FILENAME'\" with title \"nodemon\"'"
},
"watch": ["test/fixtures/", "test/samples/"],
"env": {
"NODE_ENV": "development"
},
"ext": "js,json"
}
  • nodemon.json이름으로 파일을 하나 만들고 커스텀할 수 있다
  • 무시할 디렉터리를 ignore 배열에 나열하면 된다

참고

linux node app 백그라운드 실행

1
nohup npm start &
  • ec2에서 24시간 돌아가는 웹 앱을 돌릴일이 생겼다
  • 그런데 처음에 그냥 마지막에 &만 붙여주면 되겠지 하고
  • npm start & 명령을 치고 리모트를 종료했더니, 리모트를 종료하는 순간 앱 실행도 멈췄다
  • 왜 그런가 했더니 단순히 &을 붙이고 실행한 프로세스는 터미널이 종료하면 같이 꺼진다고 한다

nohup

  • 리눅스에서는 특별한 패키지 설치없이도 완전한? 백그라운드 실행을 가능하게해주는 명령이 있다
1
2
nohup npm start &
exit
  • 이렇게 실행하고 exit을 눌러 나와주면 터미널이 자동으로 닫힌다
  • 하지만 앱은 계속해서 실행되고 있다
  • exit 은 nohup이랑 관계없고, 그냥 터미널을 종료하는 명령이다
  • nohup 명령 후 아무키나 누르면 다시 터미널을 사용할 수 있는 상태가 된다

참고

nodejs 파일 목록 출력하기

printFileList.js
1
2
3
4
5
6
const fs = require('fs');
const dirPath = '.';

fs.readdir(dirPath, (error, filelist) => {
console.log(filelist);
});
  • 은근히 자주 쓴다
  • dirPath 변수에 원하는 디렉터리의 경로를 적어주어 확인할 수 있다

내가 활용한 곳

    1. .dockerignore 파일을 추가했는데 동작할까? 궁금해서
    1. 람다 실행환경 디렉터리 구조 파악

리액트로 네이버 퍼센트 계산기 클론코딩

  • 막 배운 리액트로 처음 만들어본 프로젝트이다
  • 계산하기 귀찮아하는 나는 평소 네이버 퍼센트 계산기를 이용했었는데
  • 이 계산기를 리액트를 사용해 클론코딩해보자..

데모

후기

  • 내가 너무 간단한 것을 만들어서 그런지 모르겠지만
  • 리액트를 처음 써본 내 기준으로 너무 복잡하다고 느껴졌다
  • 기존에 html, css, js로 만들때 보다 시간이 배로 걸렸다
  • 나중에 훨씬 복잡한 것을 만들게 되었을 때 리액트가 좋다고한다
  • 이미 잘 만들어진 리액트 튜토리얼을 따라만들고, 수정하는 방식으로 만들었다
  • 사용한 개념에 대해 완전히 이해하고 익숙해지는데 시간이 필요할 것 같다

소스코드

  • react-percentage-calculator
  • README.md에 리액트로 만든 화면을 깃허브 페이지로 보여주는 방법 등 메모해놨다

sam에서 노드 모듈을 레이어로 빼기

  • 람다는 가벼운게 최고다
  • 노드 모듈은 따로 빼버리기!

Lambda Layer

  • nodejs로 치면 node_modules이다
  • 람다 함수에서 노드 모듈같은 종속성을 람다 함수 밖에으로 뺄 수 있음
  • 왜 쓰냐?
      1. 공통적으로 쓰이는 패키지들을 묶어서 관리할 수 있음
      1. 람다함수가 커지면 브라우저로 aws console에서 코드 조회를 못함

따라하기

외부 모듈을 사용해보기

  • sam template.yaml에서 layer설정할 수 있다
  • 지난번에 만든 hello world에서 이어서 진행해 보겠다
hello-world/app.js
1
2
3
// const axios = require('axios')
// const url = 'http://checkip.amazonaws.com/';
let response;
hello-world/app.js
1
2
3
const axios = require('axios');
const url = 'http://checkip.amazonaws.com/';
let response;
  • 맨 윗 2줄 주석 해제한다
hello-world/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.lambdaHandler = async (event, context) => {
try {
// const ret = await axios(url);
response = {
statusCode: 200,
body: JSON.stringify({
message: 'hello world',
// location: ret.data.trim(),
}),
};
} catch (err) {
console.log(err);
return err;
}

return response;
};
hello-world/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.lambdaHandler = async (event, context) => {
try {
const ret = await axios(url);
response = {
statusCode: 200,
body: JSON.stringify({
message: 'hello world',
location: ret.data.trim(),
}),
};
} catch (err) {
console.log(err);
return err;
}

return response;
};
  • 여기도 2곳 주석을 해제한다

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "hello_world",
"version": "1.0.0",
"description": "hello world sample for NodeJS",
"main": "app.js",
"repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs",
"author": "SAM CLI",
"license": "MIT",
"dependencies": {
"axios": "^0.18.0"
},
"scripts": {
"test": "mocha tests/unit/"
},
"devDependencies": {
"chai": "^4.2.0",
"mocha": "^6.1.4"
}
}
  • 필요없는 devDependencies 를 없앤다
  • test 할때 필요한 패키지인데, 우리는 안쓸거다
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "hello_world",
"version": "1.0.0",
"description": "hello world sample for NodeJS",
"main": "app.js",
"repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs",
"author": "SAM CLI",
"license": "MIT",
"dependencies": {
"axios": "^0.18.0"
},
"scripts": {
"test": "mocha tests/unit/"
}
}
  • 없애면 이런 모양이 된다

cmd
1
2
cd hello-world
npm i
  • app.js가 있는 hello-world 디렉터리로 와서 종속성을 설치한다
1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── .aws-sam/
├── events
│ └── event.json
├── hello-world
│ ├── node_modules/
│ ├── tests/
│ ├── .npmignore
│ ├── app.js
│ └── package.json
├── .gitignore
├── README.md
└── template.yaml
  • node_modules 폴더가 생기면서 현재 디렉터리 구조는 이렇게 된다
  • hello-world/tests 폴더를 삭제한다. 우리는 안쓴다
  • events 폴더를 삭제한다. 우리는 안쓴다
  • .aws-sam 폴더를 삭제한다. 이 폴더가 있으면 local start-api 했을때 .aws-sam를 우선 참조하기때문에 소스 코드에 변경사항이 발생해도 반영되지 않는다
1
2
3
4
5
6
7
8
9
.
├── hello-world
│ ├── node_modules/
│ ├── .npmignore
│ ├── app.js
│ └── package.json
├── .gitignore
├── README.md
└── template.yaml
  • 현재까지 디렉터리 구조는 이렇게 된다

cmd
1
2
cd ..
sam local start-api --skip-pull-image
cmd log
1
2
3
4
5
6
7
C:\tmp\hello-world\hello-world>cd ..

C:\tmp\hello-world>sam local start-api --skip-pull-image
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM
CLI if you update your AWS SAM template
2020-09-21 14:49:47 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
  • 이렇게 location에 자신의 공인 IP가 뜨면 성공이다

  • 주석 해제한 소스 코드는

  • axios라는 모듈로 http://checkip.amazonaws.com/ 에서 공인 ip를 얻어와 response 객체에 location이라는 이름으로 담아서 반환한 것이다

  • http://checkip.amazonaws.com/ 에 접속해 보면 알 수 있듯이 자신의 공인 ip를 알려주는 api다

  • 여기까지 axios라는 모듈을 이용해봤다

  • 이제 sam build & sam deploy 를 통해 aws에 올려보자

cmd
1
sam build & sam deploy
  • 배포가 완료되면 엔드 포인트를 통해서 aws에 올린 람다를 실행해보자
  • 그러면 이상한 아이피가 나온다
  • 이게 aws 컴퓨터중에서 현재 람다가 실행된 컴퓨터의 공인 ip 주소이다
  • 여기까지 잘 작동되는 것이 확인 되었다

외부 모듈을 layer로 빼기

  • 이제 브라우저를 열고 aws console > lambda > HelloWorldFunction 으로 가서 함수가 어떻게 생겻는지 확인한다
  • 함수 안에 노드 모듈이 포함되어 있는 것을 확인할 수 있다
  • 이제 이 글의 핵심인 node_modules 폴더를 layer로 빼볼것이다
  • 레이어를 만드는 방법이 여러가지가 있지만 나는 내 방식을 설명해 보겠다
  • 나는 cmd명령을 사용할 것이다

  • template.yaml에서 빨간 네모 부분을 추가한다
template.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
hello-world

Sample SAM Template for hello-world

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3

Resources:
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
Layers:
- !Ref DependencyLayer
CodeUri: hello-world/
Handler: app.lambdaHandler
Runtime: nodejs12.x
Events:
HelloWorld:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /hello
Method: get
DependencyLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: HelloWorldFunction-layer
Description: Dependencies for HelloWorldFunction
ContentUri: opt/
CompatibleRuntimes:
- nodejs12.x
RetentionPolicy: Retain

Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
HelloWorldApi:
Description: 'API Gateway endpoint URL for Prod stage for Hello World function'
Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/'
HelloWorldFunction:
Description: 'Hello World Lambda Function ARN'
Value: !GetAtt HelloWorldFunction.Arn
HelloWorldFunctionIamRole:
Description: 'Implicit IAM Role created for Hello World function'
Value: !GetAtt HelloWorldFunctionRole.Arn
  • 최종코드는 이러하다
  • 눈여겨 봐야할 것은
  • Resources아래에 DependencyLayer라는 이름으로 layer정의하고
  • ContentUri: opt/ 를 지정한다. 이 opt 디렉터리에 우리가 layer로 따로뺄 패키지가 들어가게 될것이다
  • 이제 배치파일을 2개 만들거다
layer.bat
1
2
3
4
5
6
7
echo layer process

set functionDir=HelloWorldFunction

echo y|rmdir /s opt\nodejs
mkdir opt\nodejs
move .aws-sam\build\%functionDir%\node_modules opt\nodejs
  • layer.bat을 sam project 최상위에 만들고 내용은 위와같이 한다
  • set functionDir=HelloWorldFunction 에서는 template.yaml에서 지정한 함수명을 적어주면 된다. template.yaml을 안거드렸으면 HelloWorldFunction이니까 그대로 사용하면 된다
template.yaml
1
2
3
Resources:
HelloWorldFunction: // <- 이름과 매칭되도록
Type: AWS::Serverless::Function
deploy.bat
1
2
3
echo deploy

sam build & layer.bat & sam deploy
  • 또 deploy.bat을 sam project 최상위에 만들고 내용은 위와같이 한다
  • build하고 방금 만든 layer.bat으로 배포될 빌드 디렉터리에서 node_modules을 opt 디렉터리로 옮기고
  • deploy한다

  • 결과는 이렇게 된다
  • 1.2. 디렉터리 구조를 봐보면 node_modules 폴더가 없다
  • 3.4. yaml에서 지정한 layer 등록되어 있는 것을 확인할 수 있다
  • 당연하게도 이 함수는 잘 동작한다
  • 여기까지 batch파일을 활용해서 노드모듈을 레이어로 빼는 방법을 알아보았다
  • 여기까지 프로젝트 파일

로컬에서 sam 테스트하기

  • 이제 테스트할 때 build, deploy 기다리기… 할 필요가 없다!

전제조건


sam local start-api

cmd
1
sam local start-api --skip-pull-image
cmd log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C:\tmp\hello-world>sam local start-api --skip-pull-image
Mounting HelloWorldFunction at http://127.0.0.1:3000/hello [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM
CLI if you update your AWS SAM template
2020-09-21 13:40:36 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
2020-09-21 13:40:41 127.0.0.1 - - [21/Sep/2020 13:40:41] "GET / HTTP/1.1" 403 -
2020-09-21 13:40:41 127.0.0.1 - - [21/Sep/2020 13:40:41] "GET /favicon.ico HTTP/1.1" 403 -
Invoking app.lambdaHandler (nodejs12.x)
Requested to skip pulling images ...

Mounting C:\tmp\hello-world\.aws-sam\build\HelloWorldFunction as /var/task:ro,delegated inside runtime container
START RequestId: 00b5b190-1c8d-184f-5ab5-c0e0764041d3 Version: $LATEST
END RequestId: 00b5b190-1c8d-184f-5ab5-c0e0764041d3
REPORT RequestId: 00b5b190-1c8d-184f-5ab5-c0e0764041d3 Init Duration: 438.88 ms
Duration: 4.53 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 44 MB
No Content-Type given. Defaulting to 'application/json'.
2020-09-21 13:40:48 127.0.0.1 - - [21/Sep/2020 13:40:48] "GET /hello HTTP/1.1" 200 -
2020-09-21 13:40:48 127.0.0.1 - - [21/Sep/2020 13:40:48] "GET /favicon.ico HTTP/1.1" 403 -
  • 나는 이렇게 명령을 쓴다
  • 나는 보통 매 실행마다 3-4초 정도 걸린다
  • build, deploy 하면서 테스트할때보다 획기적으로 빠르게 로컬에서 테스트 해볼 수 있다
  • --skip-pull-image옵션을 주는 이유는 매 실행마다 이미지를 받아오는 과정이 있는게 그걸 스킵하면 2초정도 빨라진다
  • 변경 사항은 자동으로 반영된다
  • 단점 ; 근데 변경사항이 없어도 매 실행 3초이상 걸린다
  • template.yaml 파일을 수정하면 수동으로 재시작 해줘야한다

samconfig.toml 에서 sam deploy 자동 y 설정하기

cmd log
1
2
3
Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y
  • deploy를 자주하다보면 이 y 누르는게 힘들다…

samconfig.toml 설정하기

samconfig.toml
1
2
3
4
5
6
7
8
9
10
version = 0.1
[default]
[default.deploy]
[default.deploy.parameters]
stack_name = "test-sam-app"
s3_bucket = "00000000000000000000000000000000000000000000000000000000000"
s3_prefix = "test-sam-app"
region = "ap-northeast-2"
confirm_changeset = false
capabilities = "CAPABILITY_IAM"
  • 처음 배포할때 -g옵션에서 설정했던것이 samconfig.toml에 저장되어있다
1
confirm_changeset = false
  • 여기서 confirm_changeset을 flase로 바꿔주면 중간에 확인 과정을 생략할 수 있다
  • 편안..!