Home

Gabriel's Blog

<맨 위로>

인강 사이트를 파싱하고 리퀘스트를 분석해보자!

서론

지금까지 ‘개요’라고 했는데 생각해보니 서론이 더 맞는 말 같다. 모 온라인 교육 사이트는 유저의 강의 수료 여부를 저장하고 확인하는 기능을 제공한다. 내가 강의를 들었는지, 듣지 않았는지 확인할 수 있는 것이다. 동영상을 전부 보았는지 어떻게 확인할까? 그 구조를 알면 강의를 직접 시청하지 않고도 수료 표시를 받을 수 있다.

주의사항

본 게시글은 편법을 통해 수료 인증을 받는 행위를 조장하거나 권장하는 의도로 작성된 것이 아니며, 웹 분석 과정에서 적용되는 다양한 Js 기능 및 개념을 기록하기 위해 작성하였습니다. 그러나 추후 문제가 발생할 경우 삭제될 수 있습니다.


구조 살펴보기: 동영상

강의 시청 버튼을 누르면 동영상 팝업창이 뜨고, 강의를 재생할 수 있다.

동영상이 끝날 때 완료 메시지를 주는데, 임의로 동영상의 속도나 위치를 조정할 수 없는 custom UI가 적용되어 있다.

<video id="media-video" style="width:100%;height:;">
<source src="https://~~~/.../***.mp4" type="video/mp4">
</video>

해당 엘리먼트를 확인하니 역시나 <video>태그다.

기본 컨트롤 패널을 표시하는 controls옵션을 추가하자.

video

마음대로 동영상 위치를 바꿀 수 있는 기본 컨트롤 패널을 볼 수 있다.

이 슬라이더를 오른쪽으로 잡아당기면

성공

성공적으로 완료처리가 되는 것을 볼 수 있다.


콘솔창 명령어

// 동영상에 컨트롤 패널을 추가
document.querySelector('video').setAttribute('controls','')

제이쿼리를 사용해도 되지만 쿼리셀렉터를 썼다. 비디오 태그에 controls속성을 추가한다.


시험도 있다

한 과목의 모든 강의를 수강하면 시험을 칠 수 있다.

그런데 이 시스템이 굉장히 특이한 게, 최종 제출 이전에 자신의 선택과 정답을 확인하고 고칠 수 있다는 점이다.

이를 보다 편리하게 이용하는 명령어를 작성해 보자.

즉,

  1. 답을 1번으로 찍고
  2. 실제 정답을 확인하고 기록한 뒤
  3. 원래 페이지로 돌아가 이 정답을 작성하는 명령어를 만들자!

우선 웹페이지의 핵심 html 코드이다.

<ul class="con-list">
    <li class="tit-i-01">총25문항 - 통과 기준 점수:20</li>
    <li>
        <p class="line-tit"><span>1. </span>&nbsp;&nbsp;문제 내용 </p>
        <ul>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
        </ul>
    </li>
    <li class="padding"></li>
    <li>
        <p class="line-tit"><span>2. </span>&nbsp;&nbsp;문제 내용 </p>
        <ul>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
            <li class="check">
                <input type="radio" attr...>
                <label for="a1">선지 내용</label>
            </li>
        </ul>
    </li>
    <li class="padding"></li>
    ...
    <li class="line-b"></li>
</ul>

<ul> 내부의 <li>요소가 각 문제에 해당하고, 문제별로 다시 <ul> 내부에 라디오버튼 또는 체크박스 가 위치해 있다.

제이쿼리를 이용해 이들을 찾고, 전부 1번으로 밀어 보자.

// 답안을 1번으로 밀기
$('ul.con-list > li > ul > li:first-child > input').prop('checked', true)

li:first-child에서 가상 태그가 사용되었다. 형제 태그 중 첫째만을 선택한다.

prop()메소드로 checked항목을 변경하는 부분도 보인다.

이렇게 한 뒤 제출하면

<ul class="con-list">
    <li class="tit-i-01">총n문항 - 통과 기준 점수:x</li>
    <li>
        <p class="line-tit"><span>1. </span>&nbsp;&nbsp;질문 내용 </p>
        <ul>
            <li class="num ">
                <span>1)</span> 선지 내용
            </li>
            <li class="num ">
                <span>2)</span> 선지 내용
            </li>
            <li class="num ">
                <span>3)</span> 선지 내용
            </li>
            <li class="num bak01 ">
                <span>4)</span> 선지 내용
            </li>
        </ul>
    </li>
    <li class="padding"></li>
    <li>
        <p class="line-tit"><span>2. </span>&nbsp;&nbsp;질문 내용 </p>
        <ul>
            <li class="num ">
                <span>1)</span> 선지 내용
            </li>
            <li class="num ">
                <span>2)</span> 선지 내용
            </li>
            <li class="num ">
                <span>3)</span> 선지 내용
            </li>
            <li class="num bak01 ">
                <span>4)</span> 선지 내용
            </li>
        </ul>
    </li>
    <li class="padding"></li>
    ...
    <li class="line-b"></li>
</ul>

같은 html 코드를 얻는다. 틀린 문제는 bak01이라는 클래스로 정답표시를 해 주고 있다.

이를 추출하자.

// 실제 답을 저장하는 배열 파싱. 생성 코드를 출력
builder = 'ans=['
$('ul.con-list>li').not('.padding,[style],.line-b,[class]').each((i,e)=>{
    builder += '['
    right = $(e).children('ul').children('li.bak01')
    right.each((j,e)=>{
        builder += `${$(e).index()},`
    })
    if(!right.length){
        builder += '0,'
    }
    builder += '],'
})
builder += ']'
console.log(builder)

.not()메서드를 사용해서 원하지 않는 태그를 걸러내는 부분 과,

만약 어떤 문항에 아무런 표시가 없다면 정답 임을 의미하므로 1번(인덱스 0)을 추가하는 부분에 주목하자.

그럼 다음과 같이 콘솔창에 정답 배열을 출력한다.

ans=[[1,],[2,],[2,],[0,],[3,],[2,],[3,],[3,],[0,],[3,],[1,],[2,],[3,],[0,],[2,],[1,],[2,],[0,],[0,],[3,],[3,],[2,],[2,],[3,],[0,],]

이제 원래 페이지로 돌아가 바로 위의 코드를 그대로 붙여넣는다. 그 후 아래의 코드를 입력한다.

$('ul.con-list > li > ul > li > input').prop('checked', false)
$('ul.con-list>li').not('.padding,[style],.line-b,[class]').each((i,e)=>{
    ans[i].forEach((x,j,a)=>{
        $(e).children('ul').children('li:eq('+x+')').children('input').prop('checked', true)
    })
})

우선 모든 선택지를 초기화한 뒤, 앞서 선언한 배열에 따라 정답을 체크해 나간다.

children('li:eq('+x+')')에서, jQuery로 n번째 요소를 선택하는 eq()선택자가 등장한다.

살짝 복잡하지만 이러한 방법으로 응시할 수 있다.


축약 버전

// 1번으로 밀기
$('ul.con-list > li > ul > li:first-child > input').prop('checked', true)
// 정답 배열 생성
b='ans=[';$('ul.con-list>li').not('.padding,[style],.line-b,[class]').each((i,e)=>{b+='[',r=$(e).children('ul').children('li.bak01'),r.each((j,e)=>{b+=`${$(e).index()},`});if(!r.length){b+='0,'}b += '],'});console.log(b+']')
// 실제 정답 선택
$('ul.con-list>li>ul>li:first-child>input').prop('checked', false);$('ul.con-list>li').not('.padding,[style],.line-b,[class]').each((i,e)=>{ans[i].forEach((x,j,a)=>{$(e).children('ul').children(`li:eq(${x})`).children('input').prop('checked', true)})})


두 번째 웹사이트

이번엔 더 정교한 방식을 사용하는 경우다.

강의 시작 시각과 종료 시각을 비교하여 동영상 길이보다 짧을 경우 에러를 표시한다.

동시에 두 개의 강의를 시청하는 것도 허용되지 않는다.

분석 결과, 각 시각은 요청이 들어온 순간을 기준으로 서버에서 측정 하므로 이를 조작하는 것은 매우 어려우며, 불법이므로 시도하지도 않았다.

결국 자동으로 강의를 수강하는 매크로를 만드는 게 최선이다.

악용 방지를 위해 웹페이지 주소를 가려 뒀다.


요청 분석

강의 시작 버튼을 누르면 클라이언트에서 다음과 같은 요청을 서버에 송신한다.

https://~~~.***.ac.kr/pages/ajax__update_time?cos_id=33&order=1&id=&type=s

order이 강의 넘버, type=s는 정황상 start 를 의미하는 듯하다.

강의가 종료되면 클라이언트에서 다음과 같은 요청을 송신한다.

https://~~~.***.ac.kr/pages/ajax__update_time?cos_id=33&order=1&id=&type=e

type=e는 역시 end 로 추정된다.


요청 모방 함수 작성

fetch로 get요청을 보낼 수 있다. 프로미스를 리턴한다.

// 강의 시작 요청 함수
function start(n){
    var url = 'https://humanrights.kaist.ac.kr/pages/ajax__update_time?cos_id=33&order='+String(n)+'&id=&type=s'
    fetch(url).then((res)=>{
        console.log(res);
    })
}

// 강의 종료 요청 함수
function end(n){
    var url = 'https://~~~.***.ac.kr/pages/ajax__update_time?cos_id=33&order='+String(n)+'&id=&type=e'
    fetch(url).then((res)=>{
        console.log(res);
    })
}

딜레이 추가

그러나 강의 시작 직후 종료 요청을 보내면 비정상적인 시도로 간주되어 강제 로그아웃된다.

일정 시간이 지난 후 콜백 함수를 실행시키는 setTimeOut()를 이용해 수정하자.

// 강의 종료 요청 예약 함수
function end_at(n, t){
    var url = 'https://~~~.***.ac.kr/pages/ajax__update_time?cos_id=33&order='+String(n)+'&id=&type=e'
    setTimeout(function() {
        fetch(url).then((res)=>{
            console.log(res);
        })
    }, t);
}

t는 밀리세컨드(ms)단위다.


동영상 url 파싱

// n번 강의의 동영상 url
function getVideoUrl(n){
    return $('#v'+String(n))[0].getAttribute('vurl_media')
}

html 파싱 결과이므로 추가 설명은 하지 않는다.


길이 알아내기

표준적인 방법인지는 모르겠는데, 비디오 엘리먼트를 생성하고 src속성에 url 주소를 넣으면 video.duration으로 동영상의 재생 시간을 읽을 수 있다.

// 동영상 소스 링크로부터 동영상 길이를 알아내는 함수
var video = document.createElement('video');
function getVideoDuration(src){
    video.src = src;
    setTimeout(()=>{
        console.log(video.duration);
    },3000)
}

동영상 정보를 받아오기까지 넉넉하게 3초의 지연을 주는 부분을 주목하자.


합치기

위의 모든 코드를 한데 모아서 궁극의 함수를 만들었다.

// 강의 수강을 예약 후 자동 실행
var video = document.createElement('video');
async function reserveVideos(array) {
    for(let i=0; i< array.length; i++){
        var n = array[i];
        await new Promise((resolve, reject)=>{
            fetch('https://~~~.***.ac.kr/pages/ajax__update_time?cos_id=33&order='+String(n)+'&id=&type=s').then((res)=>{
                console.log(String(n)+"번 강의 학습 시작");
                video.src = $('#v'+String(n))[0].getAttribute('vurl_media')
                console.log('Video source is '+video.src)
                setTimeout(()=>{
                    console.log('Video duration is '+String(video.duration))
                    var url = 'https://~~~.***.ac.kr/pages/ajax__update_time?cos_id=33&order='+String(n)+'&id=&type=e'
                    setTimeout(function() {
                        fetch(url).then((res)=>{
                            console.log(String(n)+"번 강의 학습 종료");
                            resolve();
                        })
                    }, video.duration*1000 + 2000);
                },3000)
            })
        })
    }
    console.log('모든 강의가 끝났어요! 새로고침을 눌러 보세요:>');
}
reserveVideos([1, 2, 3, 4, 5]);

함수를 호출할 때 배열을 넘기는데, 이 배열에는 수강할 강의 번호를 넣으면 된다.

asyncawait 구문으로 비동기 처리를 하는 로직이 핵심이다. 한 번에 두 강의를 수강할 수 없으므로 앞의 영상이 끝나기를 기다리는 것이다.


결론

저번에 배워 둔 자바스크립트 지식을 어디에 써먹을까 했는데 역분석도 재밌다.

그리고 왠일로 성공적인 결과가…

배가 고프기 때문에 여기서 끝!

그럼 다음에,
Gabriel-Dropout at 17:17

scribble