/** * personium.io * Copyright 2014 FUJITSU LIMITED * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.fujitsu.dc.engine.jsgi; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.core.StreamingOutput; import org.apache.http.HttpStatus; import org.mozilla.javascript.Context; import org.mozilla.javascript.FunctionObject; import org.mozilla.javascript.JavaScriptException; import org.mozilla.javascript.NativeArray; import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.ScriptableObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fujitsu.dc.engine.wrapper.DcInputStream; /** * ユーザスクリプトから返されたJSGIオブジェクト(JavaScript)をJAX-RSで返却する. */ @SuppressWarnings("serial") public final class DcResponse extends ScriptableObject { private static final int BUFFER_SIZE = 1024; /** ログオブジェクト. */ private static Logger log = LoggerFactory.getLogger(DcResponse.class); int status = 0; String charset = "utf-8"; Map<String, String> headers = new HashMap<String, String>(); String body = null; StreamingOutput streaming; OutputStream output; /** * ユーザスクリプトから返却されたJSGIのレスポンスをチェックし、Javaオブジェクトに変換する. * @param jsgiResponse JavaScriptのJSGIレスポンス * @return DcResponse * @throws Exception Exception */ public static DcResponse parseJsgiResponse(Object jsgiResponse) throws Exception { final DcResponse dcResponse = new DcResponse(); // レスポンス形式のチェック if (!(jsgiResponse instanceof NativeObject)) { String msg = "not NativeObject"; log.info(msg); throw new Exception(msg); } NativeObject response = (NativeObject) jsgiResponse; // statusのチェック Object oStatus = ScriptableObject.getProperty(response, "status"); if (!(oStatus instanceof Number)) { String msg = "response status illegal type."; log.info(msg + ":" + oStatus.getClass()); throw new Exception(msg); } // レスポンスコードが以下の条件にあてはまる場合はエラーとする // ・100番台 // ・301、303、307 if (isInvalidResCode((Number) oStatus)) { String msg = String.format("response status illegal type. status: %s", String.valueOf(Context.toString(oStatus))); log.info(msg + ":" + oStatus); throw new Exception(msg); } dcResponse.setStatus((int) Context.toNumber(oStatus)); // headersのチェック Object oHeaders = ScriptableObject.getProperty(response, "headers"); if (!(oHeaders instanceof NativeObject)) { String msg = "not headers"; log.info(msg); throw new Exception(msg); } NativeObject nHeaders = (NativeObject) oHeaders; Object[] os = nHeaders.getIds(); for (Object o : os) { if (!(o instanceof String)) { String msg = "header key format error"; log.info(msg); throw new Exception(msg); } String key = Context.toString(o); // Transfer-Encodingの指定は無効にする if ("Transfer-Encoding".equalsIgnoreCase(key)) { continue; } Object value = nHeaders.get(key, nHeaders); if (!(value instanceof String)) { String msg = "header value format error"; log.info(msg); throw new Exception(msg); } dcResponse.addHeader(key, Context.toString(value)); } // bodyのチェック Object oBody = ScriptableObject.getProperty(response, "body"); // 復帰値の型チェック if (!(oBody instanceof ScriptableObject)) { String msg = "response body undefined forEach."; log.info(msg); throw new Exception(msg); } final ScriptableObject scriptableBody = (ScriptableObject) oBody; // forEachが実装されているかチェック if (!ScriptableObject.hasProperty(scriptableBody, "forEach")) { String msg = "response body undefined forEach."; log.info(msg); throw new Exception(msg); } Method checkMethod = dcResponse.getForEach("bodyCheckFunction"); final Method responseMethod = dcResponse.getForEach("bodyResponseFunction"); // JavaのforEach実装をJavaScriptの関数として登録 ScriptableObject callback = new FunctionObject("bodyCheckFunction", checkMethod, dcResponse); // forEach呼び出し(返却データの型チェック用) Object[] args = {callback}; try { ScriptableObject.callMethod(scriptableBody, "forEach", args); } catch (JavaScriptException e) { log.info(e.getMessage()); throw new Exception(e.getMessage()); } // レスポンスの遅延処理登録 StreamingOutput stremingOutput = new StreamingOutput() { @Override public void write(OutputStream resStream) throws IOException { // forEachのコールバックを呼び出す準備 dcResponse.setOutput(resStream); // forEachをJavaScriptの関数として登録 ScriptableObject callback = new FunctionObject("bodyResponseFunction", responseMethod, dcResponse); // forEach呼び出し Object[] args = {callback}; ScriptableObject.callMethod(scriptableBody, "forEach", args); resStream.close(); } }; dcResponse.setBody(stremingOutput); return dcResponse; } /** * Engineとして許容しないレスポンスコードかどうかを判定する. * @param oStatus レスポンスコード(Number型) * @return true:Engineとして許容しないレスポンスコードである false:Engineとして許容できるレスポンスコードである */ private static boolean isInvalidResCode(Number oStatus) { // 以下のレスポンスコードは許容しない // ・3桁ではない // ・0番台 // ・100番台(クライアントの挙動が不安定になるため) if (!String.valueOf(Context.toString(oStatus)).matches("^[2-9]\\d{2}$")) { return true; } // 301、303、307はサーブレットコンテナでエラーになるため許容しない if (oStatus.intValue() == HttpStatus.SC_MOVED_PERMANENTLY || oStatus.intValue() == HttpStatus.SC_SEE_OTHER || oStatus.intValue() == HttpStatus.SC_TEMPORARY_REDIRECT) { return true; } return false; } /** * レスポンスヘッダを設定. * @param status ステータスコード */ private void setStatus(int status) { this.status = status; } /** * レスポンスヘッダーを追加. * @param key ヘッダ名 * @param value 値 * @throws Exception Exception */ private void addHeader(String key, String value) throws Exception { // Content-typeだったらcharsetを抜き出す。出力エンコードを知るため。 if (key.equalsIgnoreCase("content-type")) { // メディアタイプ異常のままJAX-RSフレームワークへ渡すと例外になるのでここでチェック if (!checkMediaType(value)) { String msg = "Response header parsing media type."; log.info(msg); throw new Exception(msg); } // 判定するパターンを生成 Pattern p = Pattern.compile("charset=([^;]+)"); Matcher m = p.matcher(value); if (m.find()) { String tmp = m.group(1); // charset異常のままJAX-RSフレームワークへ渡すと例外になるのでここでチェック if (!checkCharSet(tmp)) { String msg = "response charset illegal type."; log.info(msg); throw new Exception(msg); } this.charset = tmp; } } this.headers.put(key, value); } /** * メディアタイプが正常かチェック. * @param type チェック対象のメディア・タイプ * @return bool */ private static boolean checkMediaType(String type) { try { MediaType.valueOf(type); } catch (IllegalArgumentException e) { return false; } return true; } /** * char-setが正常かチェック. * @param value チェック対象のchar-set * @return bool */ private static boolean checkCharSet(String value) { return Charset.isSupported(value); } /** * JavaScriptからレスポンスで返すStremingOutputを設定する. 直接StreamingOutputを扱わずにラップしたDcStremingOutputを受け取る * @param value DcStremingOutputオブジェクト */ private void setBody(StreamingOutput value) { this.streaming = value; } /** * レスポンス生成. * @return レスポンス */ public Response build() { ResponseBuilder builder = Response.status(this.status); for (Map.Entry<String, String> header : this.headers.entrySet()) { builder.header(header.getKey(), header.getValue()); } builder.entity(this.streaming); return builder.build(); } /** * レスポンスの出力先のストリームをセットする. * @param output 出力先Stream */ private void setOutput(OutputStream output) { this.output = output; } /** * JavaScriptのforEachをJavaで処理するためのメソッド. * @param element forEachの要素が一件ずつ渡される * @param number 渡された要素のindexが渡される * @param object forEach対象のオブジェクトの全要素の配列が渡される * @throws IOException IOException */ public void bodyResponseFunction(Object element, double number, NativeArray object) throws IOException { if (element instanceof DcInputStream) { // 現状はEngine上のJavaScriptでバイナリを直接扱わず // JavaのストリームをそのままJavaScript内で扱うことで対応 DcInputStream io = (DcInputStream) element; byte[] buf = new byte[BUFFER_SIZE]; int bufLength; while ((bufLength = io.read(buf)) != -1) { this.output.write(buf, 0, bufLength); } } else { // 文字列はユーザスクリプトがContent-typeのcharsetで指定した文字エンコーディングで出力。 this.output.write(((String) element).getBytes(charset)); } } /** * JavaScriptのforEachをJavaで処理するためのメソッド(レスポンス内容チェック用). * @param element forEachの要素が一件ずつ渡される * @param number 渡された要素のindexが渡される * @param object forEach対象のオブジェクトの全要素の配列が渡される * @throws Exception Exception */ public void bodyCheckFunction(Object element, double number, NativeArray object) throws Exception { if (!(element instanceof DcInputStream) && !(element instanceof String)) { String msg = "response body illegal type."; log.info(msg); throw new Exception(msg); } } /** * JavaScriptのforEach処理をJavaで行うためのメソッド(function)を取得. * @param methodName メソッド名 * @return function * @throws Exception Exception */ private Method getForEach(String methodName) throws Exception { Method method; try { method = this.getClass().getMethod(methodName, new Class[] {Object.class, double.class, NativeArray.class}); } catch (SecurityException e) { String msg = "function not allowed."; log.warn(msg); throw new Exception(msg); } catch (NoSuchMethodException e) { String msg = "forEach function not found."; log.warn(msg); throw new Exception(msg); } return method; } @Override public String getClassName() { return "DcResponse"; } }