본문
Zotero Endpoint 개발하기 (CORS 문제해결)
Firefox를 기반으로 구현된 Zotero에는 23119 포트를 사용하는 HTTP 서버가 내장되어 있다. 이 서버를 통하여 외부 컴퓨터/프로그램에서도 HTTP 서버의 Endpoint (접속 URL정도로 이해하면 된다)를 통하여 현재 컴퓨터에서 동작하는 Zotero 내부정보/메소드에 접근할 수 있다. 이전 글에서 언급했던, 제작중인 플러그인을 더욱 확장시키기 위해서 추가적으로 Endpoint를 만들기로 하였다.
사실 Endpoint를 만드는것은 간단하다. 공식 홈페이지에 나와있듯 ('Zotero Connector HTTP Server') Endpoint의 prototype만 정의해주면 된다. 예를들어 http://127.0.0.1:23119/myAddon/helloWorld 의 주소를 갖는, 화면에 Hello world!라는 문구를 출력하는 웹페이지를 만들기 위해서는 아래와 같이 간단하게 Endpoint를 구현하면 된다.
var myEndpoint = Zotero.Server.Endpoints["/myAddon/helloWorld"] = function() {};
myEndpoint.prototype = {
"supportedMethods":["GET"],
"init":function(postData, sendResponseCallback) {
sendResponseCallback(200, "text/html", '<!DOCTYPE html><html><head/><body>Hello world!</body></html>');
}
}
하지만 기대대로 작동되지 않았고, 그에대해 삽질을 하다보니 아래와 같이 증상을 정리할 수 있었다.
1. Zotero의 Request not allowed 문제발생
위 주소로 접근하면 'Request not allowed'메시지를 출력하며 기대한 결과가 나오지 않는다는 것이었다. 해당 문제는 Zotero 5.0.71이후부터 발생하는것이며 이 링크에서 그에대한 논의를 확인할 수 있었다. 간단한 해결책은 curl 같은 non-browser를 사용하는 것이었다. 그래서 curl로 접속해봤더니 역시 standalone 프로그램에서 요청했을때는 문제가 없었다. 헤더의 User-Agent 를 보고 Mozilla 계열이면 접근을 막는다고 해서, 그러면 웹브라우저에서 agent만 바꿔치기 하면 되겠구나 생각했고, 그것이 문제였다.
2. 동일 domain이 아니라서 CORS 문제 발생
한참전부터 frontend쪽을 보지 않아서 익숙한대로 XMLHttpRequest / jQuery기반으로 요청하려고 살펴보다가 요즘에는 fetch()를 쓴다는것을 알게되었다. headers 옵션에 User-Agent를 수정해서 넣으면 끝이라 쉽네, 했는데 뭐가 잘 안됐다. 가만보니 내가 호출하는 페이지는 http://127.0.0.1:8887 에서 동작하고 http://127.0.0.1:23119에 정보를 요청하는거라 동일 domain이 아니어서 CORS 문제가 발생했던것. 동일 domain인지 확인하는데에는 프로토콜, 호스트, 포트번호 가 일치하는지 확인하기 떄문이다. (CORS 문제를 우회하는데에 JSONP 방식을 사용할 수 있지만 이번경우에는 사용할 수 없었다 - 개발상 귀찮음으로..)
3. GET method대신 OPTIONS 이 사용됨 - CORS preflight 동작
또한 GET method를 써서 호출하는데 계속 OPTIONS method가 넘어와서, Zotero HTTP 서버에서 supportedMethods에 "GET"옆에 "OPTIONS"를 추가해줬는데 기대한대로 작동하지 않았고, 또한 OPTIONS의 헤더를 살펴보니 변경했던 User-Agent 정보가 사라졌다. 그래서 해결해보려고 찾다보니 fetch()에 mode: 'no-cors' 옵션이 있길래 이를 넣어줬더니 이번에는 또 서버로 호출이 들어오긴 하는데 브라우저의 script상에서 응답값을 얻어올 수 없었고 또한 User-Agent정보가 사라져서 들어오는바람에 위의 'Request not allowed' 문제가 지속되었다. - User-Agent를 변경하여 요청을 했기 떄문에 Non-simple CORS Request가 되어 preflight가 동작된 사례이다.
이 문제는 결국에는 Cross-Origin Resource Sharing (CORS) 에 대한 이해가 필요한 부분이었다. 장문의 글로 예시를 들며 글을 쓰다가 귀찮아져서 아래에 해결책만 간략하게 적어본다. MDN 페이지가 깔끔하게 내용을 잘 정리한것 같아서 참고하기 좋다. 아래에 추가로 읽으면 좋은 페이지들을 정리해 본다.
How does Access-Control-Allow-Origin header work?
[jQuery] Ajax 사용 - Ajax 메소드 $.ajax() $.get() .load()
[WEB] 📚 CORS 개념 💯 완벽 정리 & 해결 방법 👏
Trying to use fetch and pass in mode: no-cors
fetch() does not send headers?
CORS는 왜 이렇게 우리를 힘들게 하는걸까?
how to process fetch response from an 'opaque' type?
* Zotero 분석
Zotero 소스코드 content/zotero/xpcom/server.js 안에는 아래와 같은 부분이 있어서 Request not allowed 오류를 발생시킨다. isBrowser 변수가 user-agent 문자열이 'Mozilla/'로 시작하는지 확인하는 부분이고, 그 아래의 if문에있는 조건들을 만족하게되면 403 오류를 내뿜게 된다. if문을 벗어나면 일반적인 웹페이지 처리가 이루어진다. 조건중에서 this.pathname.startsWith('/test/') 부분을 만족하게 되면 해당 if문을 벗어날 수 있게된다. 즉, http://127.0.0.1:23119/test/* 주소를 갖게되면 브라우저를 통한 요청이든 아니든 일반적인 웹페이지 처리를 하게 된다는 것이다.
또한 CORS를 만족하기 위해서 서버의 응답헤더에 'Access-Control-Allow-origin': '*' 을 넣음으로써 어디에서 호출하더라도 잘 동작할 수 있도록 할 필요가 있었다. 결국 아래와 같이 구현하여 아래의 fetch 코드가 성공적으로 실행됨을 확인 할 수 있었다. fetch('http://127.0.0.1:23119/test/mypreview?key=27RVL6X7').then(response => response.text()).then(text => console.log(text));
var allItemsEndpoint = Zotero.Server.Endpoints["/test/mypreview"] = function() {};
allItemsEndpoint.prototype = {
"supportedMethods": ["GET"],
"init": async function(options) {
var key=options.query.key;
if(!key) return [401, "text/plain", 'key required'];
var items=await Zotero.mypreview.getInputData(key);
return [200, {'Content-Type':"text/javascript",'Access-Control-Allow-origin': '*'} , items];
}
}
댓글