백준룸즈와 익스텐션 - boostcampwm2023/baekjoonrooms GitHub Wiki

백준룸즈

백준룸즈는 leetcode를 이용한 leetrooms라는 익스텐션을 국내 사이트인 백준에 맞게 포팅한 서비스이다.

백준과 leetcode에는 차이점이 있고 익스텐션의 어려운 심사 등의 이유로 백준룸즈는 익스텐션의 기능을 최소화하려 한다.

백준룸즈에서 익스텐션의 기능

  • 익스텐션은 단순히 백준 페이지의 제출 버튼을 listen해서 특정 코드 실행
  • 백준룸즈에 로그인이 되지 않으면 익스텐션 기능 비활성화
  • 로그인을 했더라도, 방에 참가하지 않았다면 익스텐션 기능 비활성화

따라서 익스텐션은 백준과 백준룸즈를 보고있어야 한다.

익스텐션과 백준룸즈와의 관계

  • 익스텐션이 유저가 로그인 했는지 확인
  • 익스텐션이 유저가 방에 참가했는지 확인
  • 둘 다 만족된다면 익스텐션 활성화

익스텐션과 백준과의 관계

  • 익스텐션이 활성화 되어있다면 백준의 제출 버튼 클릭을 감시
  • 클릭 시 제출 정보를 서버에 전송

익스텐션

먼저 익스텐션에 대해 간단히 정리해 보면,

익스텐션은 크게 popup, content, background로 나눌 수 있다.

popup은 사용자가 브라우저의 확장 아이콘을 클릭하면 표시되는 작은 창이다.

content 스크립트는 현재 웹 페이지의 컨텐츠와 상호 작용하는 스크립트이다. DOM 조작이 가능하다.

background 스크립트는 브라우저가 실행되는 동안 계속해서 실행되는 스크립트로, 백그라운드에서 동작한다.

popup

popup 창을 만드는 방법은 manifest.json 파일에

"action": {
  "default_popup": "popup/popup.html"
},

코드를 추가 후에 popup 폴더안의 popup.html, popup.css, popup.js 파일을 통해 팝업창을 만들면 된다.

content

content 스크립트를 사용하는 방법은 manifest.json 파일에

"content_scripts": [
  {
    "js": ["scripts/content.js"], // 스크립트 파일
    "matches": ["https://www.google.com/*"] // 상호 작용할 웹 페이지 url
  }
]

를 추가해 준 뒤, 설정한 스크립트 파일에서 작업을 하면 된다.

background

Background 스크립트를 사용하는 방법은 manifest.json 파일에

"background": {
  "service_worker": "background.js"
},

를 추가해 준 뒤, 설정한 스크립트 파일에서 작업을 해주면 된다.

background는 console이 따로 있어서 background conosole에서 console.log 확인하기


폴더 구조 예시)

hjccQNanPjTDpIajkhPU

image: https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/


manifest.json 예시)

{
  "manifest_version": 3,
  "name": "Example Extension",
  "description": "Base Level Extension",
  "version": "1.0",
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup/popup.html"
  },
  "content_scripts": [
    {
      "js": ["scripts/content.js"],
      "matches": ["https://www.google.com/*"]
    }
  ]
}

위의 방식들은 manifest 버전 3을 기준으로 작성 된 것이고 manifest 버전 2는 각각의 방식이 조금씩 다르다.

Manifest 버전 2를 사용하면 다음과 같은 경고를 띄워주기 때문에 manifest 3을 사용해야 한다.

Manifest version 2 is deprecated, and support will be removed in 2023. 
See https://developer.chrome.com/blog/mv2-transition/ for more details.

manifest의 버전 각각의 방식에 조금씩 차이점이 있지만 그것에 대해서는 아직 다루지 못했다.


Extensions API

익스텐션에는 많은 API가 있다. 이 API들을 통해서 정말 많은 것을 할 수있는 것 같다. 따라서 익스텐션을 개발할 때 보안상 위험이 없나 잘 확인을 해야할 것 같다.

크롬의 공식문서에 각 API들에 대한 설명과 사용법이 있다.

샘플 코드 또한 제공되니 이것을 통해 학습을 할 수있다.

익스텐션에 대해서는 찾아보니 자료가 많이 없거나 옛날 자료가 많은 것 같다. 그래서 공식문서로 학습하는 것을 추천한다.

백준룸즈에서 가장 필요한 API는 webRequest 이므로 webRequset에 대해 학습을 하였다.


webRequest와 declarativeNetReqeust API

백준룸즈의 익스텐션에서 가장 중요한 기능은 제출 버튼을 감시해 제출 버튼 클릭 시 서버로 제출 정보를 보내주는 기능이다.

content 스크립트를 통해 백준의 제출 페이지의 dom을 조작해 제출 버튼 클릭 시 제출자 아이디와 문제 번호를 서버에 보내줄 수 도 있다.

그렇게 된다면 문제점이 제출 버튼 클릭 시 페이지가 변하는데, 페이지가 변하면서 서버로 fetch를 하는 코드가 실행이 안된다는 점이다.

dom 조작을 통해 이벤트를 잘 제어하면 문제를 해결하는게 불가능 할 것 같진 않은데 dom을 임의로 조작을 하는 것이 좋은 방법인 것 같지는 않다.

그래서 webRequest API를 살펴보았다.

webRequest

https://developer.chrome.com/docs/extensions/reference/webRequest/

webRequest에 대한 공식문서 설명은 다음과 같다.

Use the chrome.webRequest API to observe and analyze traffic and to intercept, block, or modify requests in-flight.

chrome.webRequest API를 사용하여 트래픽을 관찰 및 분석하고 in-flight 요청을 가로채거나 차단하거나 수정할 수 있습니다.

즉, webRequest는 request를 다룰 수 있는 API이다.

webRequest를 이용하면 request가 발생하려 할 때, request가 성공적으로 처리 될 때, http(s) 응답 헤더가 수신 될 때 등의 request 정보를 얻거나, 차단하거나 수정하는 등 조작할 수 있다.

차단하거나 수정 등을 할 때는 webRequestBlocking도 함께 사용해야 한다.

webRequest는 content 스크립트가 아닌 background 스크립트에서 사용할 수 있다.

사용을 위해 manifest.json에 다음을 추가 해야 한다.

{
  "name": "My extension",
  ...
  "permissions": [
    "webRequest"
  ],
  "host_permissions": [
    "*://*.google.com/*"
  ],
  ...
}

As of Manifest V3, the "webRequestBlocking" permission is no longer available for most extensions. Consider "declarativeNetRequest", which enables use of the declarativeNetRequest API. Aside from "webRequestBlocking", the webRequest API will be unchanged and available for normal use. Policy installed extensions can continue to use "webRequestBlocking".

Manifest V3 현재 대부분의 확장에서 "webRequestBlocking" 권한을 더 이상 사용할 수 없습니다. 선언형 NetRequest API를 사용할 수 있는 "declarativeNetRequest"를 고려하십시오. "webRequestBlocking" 이외에도 webRequest API는 변경되지 않고 정상적으로 사용할 수 있습니다. 정책이 설치된 확장에서는 "webRequestBlocking"을 계속 사용할 수 있습니다.

공식 문서에 위와 같은 글이 써있어 declarativeNetRequest도 한번 살펴 보았다.

declarativeNetRequest

https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/

declarativeNetRequest에 대한 공식문서 설명은 다음과 같다.

The chrome.declarativeNetRequest API is used to block or modify network requests by specifying declarative rules. This lets extensions modify network requests without intercepting them and viewing their content, thus providing more privacy.

chrome.declarativeNetRequest API는 선언 규칙을 지정하여 네트워크 요청을 차단하거나 수정하는 데 사용되며, 이를 통해 확장자가 네트워크 요청을 가로채거나 내용을 보지 않고 수정할 수 있어 더 많은 프라이버시를 제공합니다.

설명을 보면 네트워크 요청을 가로채거나 내용을 보지 않고 수정할 수 있다고 나오는데 이 부분이 webRequest와 다른 부분이다.

이 부분 때문에 v3에서 더이상 webRequestBlocking을 지원하지 않고 declarativeNetRequest를 사용하라고 추측했다.

webRequets와 declarativeNetRequest는 주로 광고 차단 등의 익스텐션에 쓰이는 API 인 것 같다.

declarativeNetRequest는 원하는 작업은 미리 JSON 파일로 선언해서 사용하는 것 같아 좀 복잡해 보인다.

백준룸즈는 요청을 차단하거나 수정하는 것은 필요 없고 요청이 발생 할 때 요청 정보만 얻으면 되기 때문에 declartiveNetRequest를 굳이 사용하지 않고 webRequest만 이용하면 된다.

webRequest 사용 결정 이유

  • webRequestBlocking은 매니페스트 v3에서 사용이 불가능하지만 그냥 webRequest는 가능하다.
  • 백준룸즈는 제출 페이지에서 제출 request가 발생할 때 그 request를 차단하거나 수정할 필요없이 그냥 서버에 보내주기만 하면 되기 때문에 webRequestBlocking이나 declarativeNetRequest를 사용할 필요가 없다. 즉, webRequest만 사용해도 충분하다.
  • webRequest는 background에서 돌아가기 때문에 제출 버튼 클릭 시 페이지가 바뀌는 것과 관련이 없다.

데모

간단한 express 서버를 구현해 익스텐션에서 백준의 제출 버튼 클릭 시 request 정보를 서버에 보내는 것을 구현해 보았다.

// server.js

const express = require("express");
const app = express();
const port = 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/test", (req, res) => {
  console.log(req.body);
});

app.listen(port, () => {
  console.log(`server start :p`);
});
// manifest.json

{
  "manifest_version": 3,
  "name": "BOJ Rooms",
  "description": "Base Level Extension",
  "version": "1.0",
  "permissions": ["webRequest"],
  "host_permissions": [
    "https://www.acmicpc.net/submit/*",  // request 발생을 감시할 웹사이트 url
    "http://localhost:3000/*"             // 제출 정보를 보낼 서버 url
  ],
  "background": {
    "service_worker": "background.js"
  }
}

chrome.webRequest.onBeforeRequest

Fires when a request is about to occur. This event is sent before any TCP connection is made and can be used to cancel or redirect requests.

요청이 발생하려고 할 때 발생합니다. 이 이벤트는 TCP 연결이 이루어지기 전에 전송되며 요청을 취소하거나 리디렉션하는 데 사용할 수 있습니다.

// background.js

chrome.webRequest.onBeforeRequest.addListener(
  function (details) {
    if (details.method === "POST") {
      fetch("http://localhost:3000/test", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(details),
      });
    }
  },
  { urls: ["https://www.acmicpc.net/submit/*"] },
  ["requestBody"]
);

"https://www.acmicpc.net/submit/*" 에서 request가 발생하려 할 때 request 정보를 알 수 있음.

option으로 "requestBody"를 줄 수 있는데 requestBody에 제출하는 소스코드 정보가 있음.

하지만 제출한 사용자의 아이디를 알 수 없음. 제출한 사용자의 아이디는 응답 해더에서 볼 수 있는데 responseHeaders 옵션은

chrome.webRequest.onBeforeRequest에서 사용 불가.


결과

스크린샷 2023-11-14 오후 4 14 03

chrome.webRequest.onHeadersReceived

Fires each time that an HTTP(S) response header is received. Due to redirects and authentication requests this can happen multiple times per request. This event is intended to allow extensions to add, modify, and delete response headers, such as incoming Content-Type headers. The caching directives are processed before this event is triggered, so modifying headers such as Cache-Control has no influence on the browser's cache. It also allows you to cancel or redirect the request.

HTTP(S) 응답 헤더가 수신될 때마다 발생합니다. 리디렉션 및 인증 요청으로 인해 이 문제는 요청당 여러 번 발생할 수 있습니다. 이 이벤트는 들어오는 Content-Type 헤더와 같은 응답 헤더를 추가, 수정 및 삭제할 수 있도록 확장을 허용하기 위한 것입니다. 캐시 지시문은 이 이벤트가 트리거되기 전에 처리되므로 캐시-제어와 같은 헤더를 수정해도 브라우저의 캐시에 영향을 미치지 않습니다. 또한 요청을 취소하거나 리디렉션할 수 있습니다.

// background.js

chrome.webRequest.onHeadersReceived.addListener(
  function (details) {
    if (details.method === "POST") {
      fetch("http://localhost:3000/test", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(details),
      });
    }
  },
  { urls: ["https://www.acmicpc.net/submit/*"] },
  ["responseHeaders"]
);

"https://www.acmicpc.net/submit/*"의 응답 헤더가 수신될 때 정보를 알 수 있음.

option으로 "responseHeaders"를 줄 수 있는데 responseHeaders에 이동할 위치의 url을 알 수 있음

=> '/status?user_id=ccp0209&problem_id=1001&from_mine=1', 제출한 사용자의 아이디를 알 수 있음.

option으로 "requestBody"를 줄 수 없어, 소스 코드 정보는 알 수 없음.


결과

스크린샷 2023-11-14 오후 4 11 02

이 정도면 백준룸즈의 익스텐션을 개발할 때 필요한 것들은 학습을 한 것 같다.

popup을 이용해 백준룸즈 웹 사이트로 이동을 할 수 있게 해주고, webRequest를 이용해 제출 버튼을 감시해 제출 정보를 서버에 보내주고, content 스크립트를 이용해 백준룸즈 웹 사이트에 로그인과 방 참가 여부를 확인해 익스텐션을 활성/비활성화를 해주면 될 것 같다.

content 스크립트와 background 스크립트는 chrome.runtime.sendMessage와 chrome.runtime.onMessage를 이용해 데이터를 주고 받으면 된다.

⚠️ **GitHub.com Fallback** ⚠️