787 lines
36 KiB
Plaintext
787 lines
36 KiB
Plaintext
<cfcomponent extends="taffy.core.api">
|
|
<!---<cfset this.name = "deck_api" />--->
|
|
<cfset this.mappings["/resources"] = expandPath("./resources")/>
|
|
<cfset this.mappings["/taffy"] = expandPath("./taffy")/>
|
|
<cfset this.mappings["/lib"] = expandPath("./lib")/>
|
|
|
|
<cflock scope="application" type="exclusive" timeout="3"><!---*** не уверен, что это необходимо--->
|
|
<cftry>
|
|
<!---production environment--->
|
|
<cfinclude template="../../conf/prod.cfm"/>
|
|
<cfcatch type="ANY">
|
|
<cftry>
|
|
<!---staging environment--->
|
|
<cfinclude template="../../conf/stage.cfm"/>
|
|
<cfcatch type="ANY">
|
|
<cftry>
|
|
<!--- development environment --->
|
|
<cfinclude template="../../conf/dev.cfm"/>
|
|
<cfcatch type="ANY">
|
|
<!---configure right here--->
|
|
<!---для ясности--->
|
|
<cfset this.config.environment = "dev-default" />
|
|
<cfset this.config.datasource = "cmdb" />
|
|
<cfset this.config.is_dev_default = "true" /><!---это виднее в сообщении об ошибке--->
|
|
</cfcatch>
|
|
</cftry>
|
|
|
|
</cfcatch>
|
|
</cftry>
|
|
</cfcatch>
|
|
</cftry>
|
|
|
|
<cfset this.datasource = this.config.datasource />
|
|
<cfset request.config = this.config />
|
|
</cflock>
|
|
|
|
<cfscript>
|
|
this.name = hash(getCurrentTemplatePath());
|
|
//variables.framework = structNew(); //
|
|
variables.framework.debugKey = "debug";
|
|
variables.framework.reloadKey = "reload";
|
|
variables.framework.reloadPassword = "true";
|
|
variables.framework.reloadOnEveryRequest = true;
|
|
variables.framework.serializer = "taffy.core.nativeJsonSerializer";
|
|
//variables.framework.serializer = "JsonUtilSerializer";
|
|
variables.framework.dashboardKey = "dashboard";
|
|
variables.framework.disableDashboard = false;
|
|
variables.framework.unhandledPaths = "/flex2gateway";
|
|
variables.framework.allowCrossDomain = true; // соответствующие заголовки, включая Access-Control-Allow-Origin выставляются только при наличии заголовка origin в реквесте
|
|
//variables.framework.allowCrossDomain = ""; // перепишем явно, возвращая Origin
|
|
|
|
//variables.framework.docs={};
|
|
variables.framework.docs.APIName="Deck API";
|
|
variables.framework.docs.APIVersion="0.045";
|
|
|
|
variables.framework.globalHeaders = structNew();
|
|
variables.framework.globalHeaders["Access-Control-Expose-Headers"] = "Location";
|
|
variables.framework.globalHeaders["Access-Control-Allow-Credentials"] = "true";
|
|
variables.framework.globalHeaders["X-Application-Version"] = variables.framework.docs.APIVersion;
|
|
//variables.framework.globalHeaders["Access-Control-Allow-Origin"] = "*";
|
|
|
|
request.UNDEFINED_USR_ID=-1;
|
|
request.ANONYMOUS_USR_ID=2;
|
|
request.GUEST_USR_ID=3
|
|
|
|
////////////////////////////////////////////////////////
|
|
request.ORCHESTRATOR_AUTH = createObject("java", "java.lang.System").getEnv("ORCHESTRATOR_AUTH");
|
|
if (isNull(request.ORCHESTRATOR_AUTH)) {
|
|
request.ORCHESTRATOR_AUTH = "Basic c3ZjX2xrdGVzdDoxMTgzZDIyNzc1OWExYjFkZmVjM2M3NTFiMzIyNzdiYTQ3";//*** времянка, дубль из deck/Application.cfm
|
|
}
|
|
//*** почему-то scope this ведет себя совершенно не так, как request и variables
|
|
// в нем, когда переменная получает значение Empty:null, можно выполнить writedump, а в других получаем ошибку, что такой переменной нет
|
|
request.locateIamService=locateIamService; //for possible debug
|
|
//request.IAM_SERVICE_URL=this.iamServiceUrl;
|
|
</cfscript>
|
|
|
|
<!--- <cfset this.orchestrator_authorization_string=createObject("java", "java.lang.System").getEnv("ORCHESTRATOR_AUTH")/>
|
|
<cfif isEmpty(this.orchestrator_authorization_string)>
|
|
<cfset this.orchestrator_authorization_string = "Basic c3ZjX2xrdGVzdDoxMTgzZDIyNzc1OWExYjFkZmVjM2M3NTFiMzIyNzdiYTQ3"/><!--- *** remove --->
|
|
</cfif> --->
|
|
|
|
<cffunction name="rethrow" returntype="void">
|
|
<!--- https://www.raymondcamden.com/2004/03/09/3089633C-9FA0-606B-3F540AE9642A795F --->
|
|
<cftry>
|
|
<cfcatch>
|
|
<cfrethrow/>
|
|
</cfcatch>
|
|
</cftry>
|
|
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="onApplicationStart">
|
|
<!--- <cfset application.dbUser = "username" />
|
|
<cfset application.dbPass = "password" />--->
|
|
<!---<cfset application._taffy.settings.allowCrossDomain = true/>--->
|
|
<cfset this.iamServiceUrl=locateIamService()/> <!--- ***при недоступности будет падать все --->
|
|
<cfreturn super.onApplicationStart() />
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="locateIamService"><!--- *** архитектурный колхоз --->
|
|
<cfloop list="https://deck-dev.ngcloud.ru/api/v1/user,https://deck-test.ngcloud.ru/api/v1/user,https://deck.ngcloud.ru/api/v1/user" index="serviceUrl"><!--- *** начиная с девелоперского --->
|
|
<cftry>
|
|
<cfhttp url=#serviceUrl# method="OPTIONS" result="resp" timeout=1/>
|
|
|
|
<cfreturn serviceUrl/>
|
|
<cfcatch type="any">
|
|
<!--- <cfdump var=#cfcatch#/> --->
|
|
</cfcatch>
|
|
</cftry>
|
|
</cfloop>
|
|
<cfthrow message="IAM service unavailable" detail="cannot find IAM"/>
|
|
</cffunction>
|
|
|
|
<!--- <cffunction name="onRequest">
|
|
<cfargument name="template" type="string" required="true"/>
|
|
<cfset request.startTickCount=getTickCount()/>
|
|
<!---<cfheader name="Access-Control-Allow-Origin" value="*"/>--->
|
|
<cfreturn super.onRequest(template) />
|
|
</cffunction> --->
|
|
|
|
<cfscript>
|
|
|
|
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers){
|
|
/* https://docs.taffy.io/#/3.5.0 */
|
|
/*
|
|
//allow white-listed requests through
|
|
|
|
if (cfc == "login"){
|
|
return true;
|
|
}
|
|
|
|
//otherwise require a device token
|
|
if (!structKeyExists(requestArguments, "deviceToken")){
|
|
return newRepresentation().noData().withStatus(401, "Authentication Required");
|
|
|
|
//and make sure it's valid
|
|
}else if (!validateToken(requestArguments.deviceToken)){
|
|
|
|
return newRepresentation().noData().withStatus(403, "Not Authorized");
|
|
}
|
|
|
|
//return representationOf(requestArguments);
|
|
//if a token is included, and valid, allow the request to continue
|
|
return true;
|
|
*/
|
|
////////////////////////////////////////////////
|
|
// самым грубым образом получаем данные от IDP, не обрабатывая исключения
|
|
|
|
|
|
//if (variables.framework.allowCrossDomain EQ "") {
|
|
//corsHeaders(); // *** нужно, чтобы variables.framework.allowCrossDomain="" иначе задвоятся заголовки
|
|
//}
|
|
|
|
//if (UCase(arguments.verb) EQ 'OPTIONS') return newRepresentation().noData().withStatus("204","No Data").withHeaders({"Content-Type":"application/json;charset=utf-8"});
|
|
if (uCase(arguments.verb) EQ 'OPTIONS') return true;
|
|
if (lCase(arguments.cfc) EQ 'err') return true;
|
|
if (lCase(arguments.cfc) EQ 'throw') return true;
|
|
|
|
var auth="";
|
|
if (structKeyExists(headers,"Authorization")) {
|
|
auth=headers.Authorization;
|
|
} else {
|
|
return representationOf("Authorization header expected").withStatus(401);
|
|
}
|
|
|
|
try {
|
|
var httpService = new http(method = "GET", charset = "utf-8", url = #this.iamServiceUrl#, timeout="3");
|
|
httpService.addParam(type = "HEADER", name = "Accept", value = "application/json");
|
|
httpService.addParam(type = "HEADER", name = "Authorization", value = "#auth#"); //passthrough
|
|
//writedump(this.iamServiceUrl);abort;
|
|
var resp = httpService.send();
|
|
//if (resp.status_code NEQ 200) throw("IDP response not OK");
|
|
//writedump(resp);abort;
|
|
var prefix = resp.getPrefix();
|
|
if (prefix.status_code NEQ 200) throw("IDP response not OK");
|
|
var result = prefix.filecontent;
|
|
//writedump(result);abort;
|
|
} catch (e) {
|
|
/*if (fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")) { //true for local debug without IDP, etc/* does not go to repository
|
|
var result='{"accounts":[{"login":"","type":"telegram"}],"avatar":["d2d2b7ac-50af-432b-b7c4-f7d6561e288a"],"company":"ООО «НУБЕС»","companyId":"8ec70ac0-546d-42a7-8cff-339c8fb51a23","contactId":"983967a3-58c2-4cdd-84a5-8b427ccfac82","email":"smishchuk@nubes.ru","externalUser":false,"fio":{"fullName":"Мищук Сергей ","name":"Сергей","secondName":"","surname":"Мищук"},"groupIds":["94bf5be7-52f4-5c13-87c3-513786934685","ab12b6b8-0265-4683-a5c2-5e1a74a55216","aff008c3-7443-50f6-86e1-ecff3cd3b04d","d6000da0-c9aa-55eb-9882-f118b432730b","d89a33dc-3177-5854-9580-f7f860a5ab7c","ddfe2555-9ef4-42cb-9a2b-7f24e61e7747","df12926f-ecb8-5479-857f-6d291464baad","fda5c295-230a-5025-9797-b8b4e99e08aa","2be9b4b6-94d1-59f9-a649-cab228d82169","64fe6f5c-a91b-5fea-a7ca-d9823701ebd4"],"integration":{"serviceId":""},"login":"smishchuk@nubes.ru","mobilePhone":[],"position":"","userId":"d72530e1-66a4-412f-a046-38153c4e5405"}';}
|
|
*/
|
|
//writedump(resp);
|
|
//return representationOf( {"exception"=e} );
|
|
return representationOf( {"exception"=e, "idpResponse"=result} );//.withStatus(500);
|
|
}
|
|
|
|
//writeDump(result); abort;
|
|
try {
|
|
var idpUserData=deserializeJson(result);
|
|
arguments.requestArguments.companyUid=idpUserData.companyId;//GUID!
|
|
arguments.requestArguments.usrUid=idpUserData.contactId;//userId; //GUID!
|
|
} catch (e) {
|
|
return representationOf( {"exception"=e, "idpResponse"=result} );
|
|
}
|
|
|
|
var usrCustomerInfo=getUsrCustomerInfo(idpUserData.contactId);
|
|
if (structIsEmpty(usrCustomerInfo)) return representationOf("Cannot find default specification for current user #result#").withStatus(422); //это создает довольно много лишних движений при отладке
|
|
arguments.requestArguments.usrId=usrCustomerInfo.usrId; //Integer!
|
|
arguments.requestArguments.specificationId=usrCustomerInfo.specificationId; //Integer
|
|
|
|
return true;
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
//ensure https because of basic authentication
|
|
if (!lcase(request.config.environment) == "development"){
|
|
if (!cgi.server_port_secure) { return newRepresentation().noData().withStatus("403.4", "SSL Required");}
|
|
}
|
|
|
|
request.usr_id=request.UNDEFINED_USR_ID;
|
|
|
|
if (checkAuth()) {return true;}
|
|
|
|
//return representationOf(arguments.headers);
|
|
//return representationOf(arguments.cfc);
|
|
var authToken="";
|
|
if (structKeyExists(arguments.headers,"x-auth-token")) {authToken=structFind(arguments.headers,"x-auth-token");}
|
|
var issuedFor="";
|
|
if (structKeyExists(arguments.headers,"x-issued-for")) {issuedFor=structFind(arguments.headers,"x-issued-for");}
|
|
if (validatePin(authToken,issuedFor, arguments.verb, arguments.cfc)) return true; //side effect: sets request.usr_id!
|
|
|
|
if (!validateCredentials(getBasicAuthCredentials())) {
|
|
return newRepresentation().noData().withStatus("401", "Invalid Credentials").withHeaders({"WWW-Authenticate":"Basic realm=mms-api"});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//private function validateToken(token){ return false; }
|
|
|
|
//refactoring required
|
|
private function validateCredentials(credentials) {//return true;
|
|
if (arguments.credentials.username == "medved" /*AND credentials.password == "ghtdtlrhjcfdxtu!Y"*/) {
|
|
request.usr_id=-3;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
</cfscript>
|
|
|
|
<!--- стоит ли сообщать отдельно об экспирации пина
|
|
нужно блокировать перебор, можно делать пару pin-issuedFor
|
|
вероятно, лучше сделать токен девайса
|
|
--->
|
|
|
|
|
|
<cffunction name="validatePin" access="private">
|
|
<cfargument name="authToken"/>
|
|
<cfargument name="issuedFor"/>
|
|
<cfargument name="verb"/>
|
|
<cfargument name="cfc"/> <!---для всех запросов, кроме GET,OPTIONS проверяем разрешенные CFC--->
|
|
|
|
<!---catch db issue--->
|
|
<cfquery name="qCheckToken">
|
|
select creator_id
|
|
from auth_token
|
|
where auth_token=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.authToken#" />
|
|
AND issued_for=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.issuedFor#" />
|
|
AND dt_valid_till > <cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#" />
|
|
</cfquery>
|
|
<!---тут можно добавить защиту от перебора с блокировкой попыток (можно добавить поле-счетчик ошибок)--->
|
|
|
|
<!--- <cfdump var=#qCheckToken#/>--->
|
|
|
|
<cfif qCheckToken.recordCount GT 0>
|
|
<cfset request.usr_id=qCheckToken.creator_id /> <!---impersonate pin issuer--->
|
|
<cfif uCase(arguments.verb) EQ "GET" OR uCase(arguments.verb) EQ "OPTIONS">
|
|
<cfreturn true/>
|
|
<cfelseif listFind("work_order,maint,maint_ls,maint_op,maint_op_ls,work_order_engineer_signature,work_order_spare_ls,work_order_spare,work_order_doc,maint_start,maint_doc_ls,maint_op_spare,maint_op_spare_ls,maint_doc", lCase(arguments.cfc))>
|
|
<cfreturn true/>
|
|
</cfif>
|
|
</cfif>
|
|
<cfreturn false/>
|
|
</cffunction>
|
|
|
|
<cffunction name="checkAuth">
|
|
<!---https://www.sjoerdlangkemper.nl/2018/09/12/authorization-header-and-cors/--->
|
|
<cfset var jwtHelper=CreateObject("component","lib.jwt").init()/>
|
|
<cfset var headers=#GetHttpRequestData().headers#/>
|
|
<cftry>
|
|
<cfset var rawToken=right(headers.Authorization,len(headers.Authorization)-len('bearer '))/>
|
|
<cfset var token=jwtHelper.decode(token=rawToken, key=request.config.IDP_certificate.keys[1], algorithms='RS256')/>
|
|
|
|
<cfset var login=token.preferred_username/>
|
|
<!---отрезаем первичный домен --->
|
|
<cfset login=ReplaceNoCase(login,"#request.config.auth_domain_suffix#","")/>
|
|
<cfquery name="local.qUsr">
|
|
select usr_id from usr where login=<cfqueryparam cfsqltype="cf_sql_varchar" value="#login#"/>
|
|
</cfquery>
|
|
|
|
<cfif #local.qUsr.recordCount# EQ 0>
|
|
<cfheader statuscode="401" statustext="User not found in local database"/>
|
|
<cfreturn false/>
|
|
</cfif>
|
|
|
|
<cfset request.usr_id=local.qUsr.usr_id/>
|
|
|
|
<cfcatch type="database">
|
|
<cfheader statuscode="500" statustext="Internal Server Error - Database Error"/>
|
|
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
|
|
<cfreturn false/>
|
|
</cfcatch>
|
|
|
|
<cfcatch type="ANY">
|
|
<cfif !GetHttpRequestData().method EQ "OPTIONS">
|
|
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
|
|
<cfreturn false/>
|
|
</cfif>
|
|
</cfcatch>
|
|
</cftry>
|
|
|
|
<cfreturn true/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="getUsrCustomerInfo">
|
|
|
|
<cfargument name="usrUid"/>
|
|
<!--- глупейшее определение спецификации по умолчанию (надо переделать, чтобы создавалась, или вообще просто записывать факты без спеки). Но помним, что при постановке на тестирование цены еше не фиксированы --->
|
|
<!--- заметим, что у нас контрагент может быть определен параллельно, от IDP --->
|
|
<cfset local={}/>
|
|
|
|
<!--- Во избежании потери времени при отладке временно сделаем создание дефолтового контракта и спецификации, если их нет --->
|
|
|
|
<cfquery name="local.qGetCustomerInfo">
|
|
select u.usr_id, s.specification_id
|
|
from usr u
|
|
join contract c on (u.contragent_id=c.contragent_id)
|
|
join specification s on (c.contract_id=s.contract_id)
|
|
where idp_usr_uid=<cfqueryparam cfsqltype="cf_sql_other" value="#arguments.usrUid#" null=#!isValid('guid',arguments.usrUid)#/>
|
|
order by specification_id desc limit 1;
|
|
</cfquery>
|
|
<!--- <cfdump var=#local.qGetCustomerInfo#/><cfabort/> --->
|
|
<cfif local.qGetCustomerInfo.recordCount GT 0>
|
|
<cfreturn {"usrId"=#local.qGetCustomerInfo.usr_id#, "specificationId"=#local.qGetCustomerInfo.specification_id#}/>
|
|
<cfelse>
|
|
<cfreturn {}/> <!--- неуклюжий способ обозначить ошибку --->
|
|
</cfif>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction name="corsHeaders">
|
|
<!--- фрагмент взят из taffy/core/api.cfc и немного переписан --->
|
|
<cfset var _taffyRequest=request._taffyRequest/>
|
|
<cfset local={}/>
|
|
|
|
<cfset local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods)) />
|
|
<cfif true
|
|
AND listFindNoCase('PUT,PATCH,DELETE,OPTIONS',_taffyRequest.verb)
|
|
AND NOT listFind(local.allowVerbs,'OPTIONS')>
|
|
<cfset local.allowVerbs = listAppend(local.allowVerbs,'OPTIONS') />
|
|
</cfif>
|
|
<cfif structKeyExists(_taffyRequest.headers, "origin") >
|
|
|
|
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
|
|
|
|
<cfheader name="Access-Control-Allow-Methods" value="#local.allowVerbs#" />
|
|
<!--- Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144 --->
|
|
<cfif not structKeyExists(_taffyRequest.headers, "Access-Control-Request-Headers")>
|
|
<cfheader name="Access-Control-Allow-Headers" value="Origin, Authorization, X-CSRF-Token, X-Requested-With, Content-Type, X-HTTP-Method-Override, Accept, Referrer, User-Agent" />
|
|
<cfelse>
|
|
<!--- parrot back all of the request headers to allow the request to continue (can we improve on this?) --->
|
|
<cfset local.allowedHeaders = {} />
|
|
<cfloop list="Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent" index="local.h">
|
|
<cfset local.allowedHeaders[local.h] = 1 />
|
|
</cfloop>
|
|
<cfset local.requestedHeaders = _taffyRequest.headers['Access-Control-Request-Headers'] />
|
|
<cfloop list="#local.requestedHeaders#" index="local.i">
|
|
<cfset local.allowedHeaders[ local.i ] = 1 />
|
|
</cfloop>
|
|
<cfheader name="Access-Control-Allow-Headers" value="#structKeyList(local.allowedHeaders)#" />
|
|
</cfif>
|
|
</cfif>
|
|
</cffunction>
|
|
|
|
<!--- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --->
|
|
<!--- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --->
|
|
|
|
<!--- Overriding TAFFY short-circuit logic (taffy/core/api.cfc) --->
|
|
<cffunction name="onRequest" output="true" returntype="boolean">
|
|
<cfargument name="targetPage" type="string" required="true" />
|
|
|
|
<cfset request.startTickCount=getTickCount()/><!--- *** --->
|
|
|
|
<cfset var _taffyRequest = {} />
|
|
<cfset var local = {} />
|
|
<cfset var m = '' />
|
|
<cfset request._taffyRequest = _taffyRequest />
|
|
<cfset local.debug = false />
|
|
|
|
<cfset _taffyRequest.metrics = {} />
|
|
<cfset m = _taffyRequest.metrics />
|
|
<cfset m.init = getTickCount() />
|
|
|
|
<!--- enable/disable debug output per settings --->
|
|
<cfif not structKeyExists(url, application._taffy.settings.debugKey)>
|
|
<cfsetting showdebugoutput="false" />
|
|
</cfif>
|
|
|
|
<!--- display api dashboard if requested --->
|
|
<cfif
|
|
NOT structKeyExists(url,application._taffy.settings.endpointURLParam)
|
|
AND NOT structKeyExists(form,application._taffy.settings.endpointURLParam)
|
|
AND len(cgi.path_info) lte 1
|
|
AND listFindNoCase(cgi.script_name, "index.cfm", "/") EQ listLen(cgi.script_name, "/")>
|
|
<cfif NOT application._taffy.settings.disableDashboard>
|
|
<cfif StructKeyExists( URL, "docs" )>
|
|
<cfinclude template="#application._taffy.settings.docsPath#" />
|
|
<cfelse>
|
|
<cfinclude template="../dashboard/dashboard.cfm" />
|
|
</cfif>
|
|
<cfabort />
|
|
<cfelse>
|
|
<cfif len(application._taffy.settings.disabledDashboardRedirect)>
|
|
<cflocation url="#application._taffy.settings.disabledDashboardRedirect#" addtoken="false" />
|
|
<cfabort />
|
|
<cfelseif application._taffy.settings.showDocsWhenDashboardDisabled>
|
|
<cfinclude template="#application._taffy.settings.docsPath#" />
|
|
<cfabort />
|
|
<cfelse>
|
|
<cfset throwError(403, "Forbidden") />
|
|
</cfif>
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<!--- get request details --->
|
|
<cfset m.beforeParse = getTickCount() />
|
|
<cfset local.parsed = parseRequest() />
|
|
<cfset m.afterParse = getTickCount() />
|
|
<cfset structAppend(_taffyRequest, local.parsed) />
|
|
<cfset m.parseTime = m.afterParse - m.beforeParse />
|
|
|
|
<!--- CORS headers (so that CORS can pass even if the resource throws an exception) --->
|
|
<cfset local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods)) />
|
|
<cfif (application._taffy.settings.allowCrossDomain eq true or len(application._taffy.settings.allowCrossDomain) gt 0)
|
|
AND listFindNoCase('PUT,PATCH,DELETE,OPTIONS',_taffyRequest.verb)
|
|
AND NOT listFind(local.allowVerbs,'OPTIONS')>
|
|
<cfset local.allowVerbs = listAppend(local.allowVerbs,'OPTIONS') />
|
|
</cfif>
|
|
<cfif structKeyExists(_taffyRequest.headers, "origin") AND (application._taffy.settings.allowCrossDomain eq true or len(application._taffy.settings.allowCrossDomain) gt 0)>
|
|
<cfif application._taffy.settings.allowCrossDomain eq true>
|
|
<!--- <cfheader name="Access-Control-Allow-Origin" value="*" /> ---><!--- *** dirty hack 2024-10-23 17:59:43--->
|
|
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
|
|
<cfelse>
|
|
<!---
|
|
The Access-Control-Allow-Origin header can only have 1 value so we check to see if the Origin header is
|
|
in the list of origins specified in the config setting and parrot back the Origin header if so.
|
|
We also need to add the Access-Control-Allow-Credentials header and set it to true for those type requests
|
|
--->
|
|
<cfset local.domains = listToArray( application._taffy.settings.allowCrossDomain, ', ;' )>
|
|
<cfif structKeyExists(_taffyRequest.headers, "origin")>
|
|
<cfloop from="1" to="#arrayLen( local.domains )#" index="local.i">
|
|
<cfif lcase( rereplace( _taffyRequest.headers.origin, "(http|https):\/\/", "", "all" ) ) EQ lcase( rereplace( local.domains[ local.i ], "(http|https):\/\/", "", "all" ) ) >
|
|
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
|
|
<cfheader name="Access-Control-Allow-Credentials" value="true" />
|
|
<cfbreak>
|
|
</cfif>
|
|
</cfloop>
|
|
</cfif>
|
|
</cfif>
|
|
<cfheader name="Access-Control-Allow-Methods" value="#local.allowVerbs#" />
|
|
<!--- Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144 --->
|
|
<cfif not structKeyExists(_taffyRequest.headers, "Access-Control-Request-Headers")>
|
|
<cfheader name="Access-Control-Allow-Headers" value="Origin, Authorization, X-CSRF-Token, X-Requested-With, Content-Type, X-HTTP-Method-Override, Accept, Referrer, User-Agent" />
|
|
<cfelse>
|
|
<!--- parrot back all of the request headers to allow the request to continue (can we improve on this?) --->
|
|
<cfset local.allowedHeaders = {} />
|
|
<cfloop list="Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent" index="local.h">
|
|
<cfset local.allowedHeaders[local.h] = 1 />
|
|
</cfloop>
|
|
<cfset local.requestedHeaders = _taffyRequest.headers['Access-Control-Request-Headers'] />
|
|
<cfloop list="#local.requestedHeaders#" index="local.i">
|
|
<cfset local.allowedHeaders[ local.i ] = 1 />
|
|
</cfloop>
|
|
<cfheader name="Access-Control-Allow-Headers" value="#structKeyList(local.allowedHeaders)#" />
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<!--- global headers --->
|
|
<cfset addHeaders(getGlobalHeaders()) />
|
|
|
|
<!---
|
|
Now we know everything we need to know to service the request. let's service it!
|
|
--->
|
|
|
|
<!--- ...after we let the api developer know all of the request details first... --->
|
|
<cfset m.beforeOnTaffyRequest = getTickCount() />
|
|
<cfset _taffyRequest.continue = onTaffyRequest(
|
|
_taffyRequest.verb
|
|
,_taffyRequest.matchDetails.beanName
|
|
,_taffyRequest.requestArguments
|
|
,_taffyRequest.returnMimeExt
|
|
,_taffyRequest.headers
|
|
,_taffyRequest.methodMetadata
|
|
,local.parsed.matchDetails.srcUri
|
|
) />
|
|
<cfset m.afterOnTaffyRequest = getTickCount() />
|
|
<cfset m.otrTime = m.afterOnTaffyRequest - m.beforeOnTaffyRequest />
|
|
|
|
<cfif not structKeyExists(_taffyRequest, "continue")>
|
|
<!--- developer forgot to return true --->
|
|
<cfthrow
|
|
message="Error in your onTaffyRequest method"
|
|
detail="Your onTaffyRequest method returned no value. Expected: Return TRUE or call noData()/representationOf()."
|
|
errorcode="400"
|
|
/>
|
|
</cfif>
|
|
|
|
<cfif isObject(_taffyRequest.continue)>
|
|
<!--- inspection complete but request has been aborted by developer; return custom response --->
|
|
<cfset _taffyRequest.result = duplicate(_taffyRequest.continue) />
|
|
<cfset structDelete(_taffyRequest, "continue")/>
|
|
<cfset m.resourceTime = 0 />
|
|
<cfelse>
|
|
<!--- inspection complete and request allowed by developer --->
|
|
|
|
<!--- handle requests for simulated responses --->
|
|
<cfif structKeyExists(_taffyRequest.requestArguments, application._taffy.settings.simulateKey) and _taffyRequest.requestArguments[application._taffy.settings.simulateKey] eq application._taffy.settings.simulatePassword>
|
|
<!--- is there a simulated response? --->
|
|
<cfset sampler = 'sample#_taffyRequest.method#Response' />
|
|
<cfif structKeyExists(_taffyRequest.matchDetails.metadata, sampler)>
|
|
<!--- get simulated response --->
|
|
<cfinvoke
|
|
component="#application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName)#"
|
|
method="#sampler#"
|
|
returnvariable="_taffyRequest.result"
|
|
/>
|
|
<cfset _taffyRequest.result = rep(_taffyRequest.result) />
|
|
<cfelse>
|
|
<!--- no method for simulated response, so return 400 --->
|
|
<cfset _taffyRequest.result = noData().withStatus(400, "No Sample Response Available") />
|
|
</cfif>
|
|
<cfelse>
|
|
<!--- send request to service --->
|
|
<cfif structKeyExists(_taffyRequest.matchDetails.methods, _taffyRequest.verb)>
|
|
<!--- check the cache before we call the resource --->
|
|
<cfset m.cacheCheckTime = getTickCount() />
|
|
<cfset local.cacheKey = getCacheKey(
|
|
_taffyRequest.matchDetails.beanName
|
|
,_taffyRequest.requestArguments
|
|
,local.parsed.matchDetails.srcUri
|
|
) />
|
|
<cfif ucase(_taffyRequest.verb) eq "GET" and validCacheExists(local.cacheKey)>
|
|
<cfset m.cacheCheckTime = getTickCount() - m.cacheCheckTime />
|
|
<cfset m.cacheGetTime = getTickCount() />
|
|
<cfset _taffyRequest.result = getCachedResponse(local.cacheKey) />
|
|
<cfset m.cacheGetTime = m.cacheGetTime - getTickCount() />
|
|
<cfelse>
|
|
<cfif ucase(_taffyRequest.verb) eq "GET">
|
|
<cfset m.cacheCheckTime = getTickCount() - m.cacheCheckTime />
|
|
<cfelse>
|
|
<cfset structDelete(m, "cacheCheckTime") />
|
|
</cfif>
|
|
<!--- returns a representation-object --->
|
|
<cfset m.beforeResource = getTickCount() />
|
|
<cfinvoke
|
|
component="#application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName)#"
|
|
method="#_taffyRequest.method#"
|
|
argumentcollection="#_taffyRequest.requestArguments#"
|
|
returnvariable="_taffyRequest.result"
|
|
/>
|
|
<cfset m.afterResource = getTickCount() />
|
|
<cfset m.resourceTime = m.afterResource - m.beforeResource />
|
|
<cfif !isDefined("_taffyRequest.result")>
|
|
<cfthrow
|
|
message="Resource did not return a value"
|
|
detail="The resource is expected to return a call to rep()/representationOf() or noData(). It appears there was no return at all."
|
|
errorcode="taffy.resources.ResourceReturnsNothing"
|
|
/>
|
|
</cfif>
|
|
<!--- If the type returned is not an instance of baseSerializer, wrap it with a call to rep().
|
|
This way we can directly return the object instead of a serializer from resource actions. --->
|
|
<cfif !isInstanceOf(_taffyRequest.result, "taffy.core.baseSerializer")>
|
|
<cfset _taffyRequest.result = rep(_taffyRequest.result) />
|
|
</cfif>
|
|
<cfif ucase(_taffyRequest.verb) eq "GET" and structKeyExists(local, "cacheKey")>
|
|
<cfset m.cacheSaveStart = getTickCount() />
|
|
<cfset setCachedResponse(local.cacheKey, _taffyRequest.result) />
|
|
<cfset m.cacheSaveTime = getTickCount() - m.cacheSaveStart />
|
|
</cfif>
|
|
</cfif>
|
|
<cfelseif NOT listFind(local.allowVerbs,_taffyRequest.verb)>
|
|
<!--- if the verb is not implemented, refuse the request --->
|
|
<cfheader name="ALLOW" value="#local.allowVerbs#" />
|
|
<cfset throwError(405, "Method Not Allowed") />
|
|
<cfelse>
|
|
<!--- create dummy response for cross domain OPTIONS request --->
|
|
<cfset _taffyRequest.resultHeaders = structNew() />
|
|
<cfset _taffyRequest.statusArgs = structNew() />
|
|
<cfset _taffyRequest.statusArgs.statusCode = 200 />
|
|
<cfset _taffyRequest.statusArgs.statusText = 'OK' />
|
|
</cfif>
|
|
</cfif>
|
|
|
|
</cfif>
|
|
<!--- make sure the requested mime type is available --->
|
|
<cfif not mimeSupported(_taffyRequest.returnMimeExt)>
|
|
<cfset throwError(400, "Requested format not available (#_taffyRequest.returnMimeExt#)") />
|
|
</cfif>
|
|
|
|
<cfif structKeyExists(_taffyRequest,'result')>
|
|
<!--- get status code --->
|
|
<cfset _taffyRequest.statusArgs = structNew() />
|
|
<cfset _taffyRequest.statusArgs.statusCode = _taffyRequest.result.getStatus() />
|
|
<cfset _taffyRequest.statusArgs.statusText = _taffyRequest.result.getStatusText() />
|
|
<!--- get custom headers --->
|
|
<cfinvoke
|
|
component="#_taffyRequest.result#"
|
|
method="getHeaders"
|
|
returnvariable="_taffyRequest.resultHeaders"
|
|
/>
|
|
</cfif>
|
|
|
|
<cfsetting enablecfoutputonly="true" />
|
|
<cfcontent reset="true" type="#getReturnMimeAsHeader(_taffyRequest.returnMimeExt)#; charset=utf-8" />
|
|
<cfheader statuscode="#_taffyRequest.statusArgs.statusCode#" statustext="#_taffyRequest.statusArgs.statusText#" />
|
|
|
|
<!--- headers --->
|
|
<cfset addHeaders(_taffyRequest.resultHeaders) />
|
|
|
|
<!--- add ALLOW header for current resource, which describes available verbs --->
|
|
<cfheader name="ALLOW" value="#local.allowVerbs#" />
|
|
|
|
<!--- metrics headers that should always apply --->
|
|
<cfheader name="X-TIME-IN-PARSE" value="#m.parseTime#" />
|
|
<cfheader name="X-TIME-IN-ONTAFFYREQUEST" value="#m.otrTime#" />
|
|
<cfif structKeyExists(m, "resourceTime")>
|
|
<cfheader name="X-TIME-IN-RESOURCE" value="#m.resourceTime#" />
|
|
</cfif>
|
|
<cfif structKeyExists(m, "cacheCheckTime")>
|
|
<cfheader name="X-TIME-IN-CACHE-CHECK" value="#m.cacheCheckTime#" />
|
|
</cfif>
|
|
<cfif structKeyExists(m, "cacheGetTime")>
|
|
<cfheader name="X-TIME-IN-CACHE-GET" value="#m.cacheGetTime#" />
|
|
</cfif>
|
|
<cfif structKeyExists(m, "cacheSaveTime")>
|
|
<cfheader name="X-TIME-IN-CACHE-SAVE" value="#m.cacheSaveTime#" />
|
|
</cfif>
|
|
|
|
<cfif application._taffy.settings.exposeHeaders>
|
|
<cfset local.exposeHeaderList = structKeyList(_taffyRequest.resultHeaders) />
|
|
<cfset local.exposeHeaderValue = "" />
|
|
<cfif application._taffy.settings.useEtags and _taffyRequest.verb eq "GET" and _taffyRequest.result.getType() eq "textual">
|
|
<cfset local.exposeHeaderList = listAppend(local.exposeHeaderList, "Etag") />
|
|
</cfif>
|
|
<cfloop list="#local.exposeHeaderList#" index="local.exposeHeader">
|
|
<!--- filter out default simple response headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers --->
|
|
<cfif not listFindNoCase("Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", local.exposeHeader)>
|
|
<cfset local.exposeHeaderValue = listAppend(local.exposeHeaderValue, local.exposeHeader) />
|
|
</cfif>
|
|
</cfloop>
|
|
<cfif listLen(local.exposeHeaderValue) gt 0>
|
|
<cfheader name="Access-Control-Expose-Headers" value="#local.exposeHeaderValue#" />
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<!--- result data --->
|
|
<cfif structKeyExists(_taffyRequest,'result')>
|
|
<cfset _taffyRequest.resultType = _taffyRequest.result.getType() />
|
|
<cfset local.resultSerialized = '' />
|
|
|
|
<cfif _taffyRequest.resultType eq "textual">
|
|
<!--- serialize the representation's data into the requested mime type --->
|
|
<cfset _taffyRequest.metrics.beforeSerialize = getTickCount() />
|
|
<cfinvoke
|
|
component="#_taffyRequest.result#"
|
|
method="getAs#_taffyRequest.returnMimeExt#"
|
|
returnvariable="_taffyRequest.resultSerialized"
|
|
/>
|
|
<cfset _taffyRequest.metrics.afterSerialize = getTickCount() />
|
|
<cfset m.serializeTime = m.afterSerialize - m.beforeSerialize />
|
|
<cfheader name="X-TIME-IN-SERIALIZE" value="#m.serializeTime#" />
|
|
|
|
<!--- apply jsonp wrapper if requested --->
|
|
<cfif structKeyExists(_taffyRequest, "jsonpCallback")>
|
|
<cfset _taffyRequest.resultSerialized = _taffyRequest.jsonpCallback & "(" & _taffyRequest.resultSerialized & ");" />
|
|
</cfif>
|
|
|
|
<!--- don't return data if etags are enabled and the data hasn't changed --->
|
|
<cfif application._taffy.settings.useEtags and _taffyRequest.verb eq "GET">
|
|
<!--- etag values are quoted per: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag --->
|
|
<cfif structKeyExists(server, "lucee")>
|
|
<!--- hashCode() will not work for lucee, see issue #354 --->
|
|
<cfset _taffyRequest.serverEtag = '"' & hash(_taffyRequest.resultSerialized) & '"' />
|
|
<cfelse>
|
|
<cfset _taffyRequest.serverEtag = '"' & _taffyRequest.result.getData().hashCode() & '"' />
|
|
</cfif>
|
|
<cfif structKeyExists(_taffyRequest.headers, "If-None-Match")>
|
|
<cfset _taffyRequest.clientEtag = _taffyRequest.headers['If-None-Match'] />
|
|
|
|
<cfif len(_taffyRequest.clientEtag) gt 0 and _taffyRequest.clientEtag eq _taffyRequest.serverEtag>
|
|
<cfheader statuscode="304" statustext="Not Modified" />
|
|
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
|
|
<cfreturn true />
|
|
<cfelse>
|
|
<cfheader name="Etag" value="#_taffyRequest.serverEtag#" />
|
|
</cfif>
|
|
<cfelse>
|
|
<cfheader name="Etag" value="#_taffyRequest.serverEtag#" />
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<cfset m.done = getTickCount() />
|
|
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.serializeTime />
|
|
<cfif structKeyExists(m, "resourceTime")>
|
|
<cfset m.taffyTime -= m.resourceTime />
|
|
</cfif>
|
|
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
|
|
|
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
|
|
<cfif _taffyRequest.resultSerialized neq ('"' & '"')>
|
|
<cfset local.resultSerialized = _taffyRequest.resultSerialized />
|
|
</cfif>
|
|
<!--- debug output --->
|
|
<cfif structKeyExists(url, application._taffy.settings.debugKey)>
|
|
<cfset local.debug = true />
|
|
</cfif>
|
|
|
|
<cfelseif _taffyRequest.resultType eq "filename">
|
|
<cfset m.done = getTickCount() />
|
|
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
|
|
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
|
<cfcontent reset="true" file="#_taffyRequest.result.getFileName()#" type="#_taffyRequest.result.getFileMime()#" deletefile="#_taffyRequest.result.getDeleteFile()#" />
|
|
|
|
<cfelseif _taffyRequest.resultType eq "filedata">
|
|
<cfset m.done = getTickCount() />
|
|
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
|
|
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
|
<cfcontent reset="true" variable="#_taffyRequest.result.getFileData()#" type="#_taffyRequest.result.getFileMime()#" />
|
|
|
|
<cfelseif _taffyRequest.resultType eq "imagedata">
|
|
<cfset m.done = getTickCount() />
|
|
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
|
|
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
|
|
<cfcontent reset="true" variable="#_taffyRequest.result.getImageData()#" type="#_taffyRequest.result.getFileMime()#" />
|
|
|
|
</cfif>
|
|
</cfif>
|
|
|
|
<cfset local.resultSerialized = "" />
|
|
<cfif structKeyExists( _taffyRequest, "resultSerialized" )>
|
|
<cfset local.resultSerialized = _taffyRequest.resultSerialized />
|
|
</cfif>
|
|
|
|
<cfset local.result = StructNew() />
|
|
<cfif structKeyExists( _taffyRequest, "result" )>
|
|
<cfset local.result = _taffyRequest.result.getData() />
|
|
</cfif>
|
|
|
|
<!--- ...after the service has finished... --->
|
|
<cfset m.beforeOnTaffyRequestEnd = getTickCount() />
|
|
<cfset onTaffyRequestEnd(
|
|
_taffyRequest.verb
|
|
,_taffyRequest.matchDetails.beanName
|
|
,_taffyRequest.requestArguments
|
|
,_taffyRequest.returnMimeExt
|
|
,_taffyRequest.headers
|
|
,_taffyRequest.methodMetadata
|
|
,local.parsed.matchDetails.srcUri
|
|
,local.resultSerialized
|
|
,local.result
|
|
,_taffyRequest.statusArgs.statusCode
|
|
) />
|
|
<cfset m.otreTime = getTickCount() - m.beforeOnTaffyRequestEnd />
|
|
<cfheader name="X-TIME-IN-ONTAFFYREQUESTEND" value="#m.otreTime#" />
|
|
|
|
<cfif len(trim(local.resultSerialized))>
|
|
<cfoutput>#local.resultSerialized#</cfoutput>
|
|
</cfif>
|
|
<!--- debug output --->
|
|
<cfif local.debug>
|
|
<cfoutput><h3>Request Details:</h3><cfdump var="#_taffyRequest#"></cfoutput>
|
|
</cfif>
|
|
|
|
<cfreturn true />
|
|
</cffunction>
|
|
|
|
|
|
<!--- <cffunction name="checkForLocalDebug">
|
|
<cfreturn fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")>
|
|
</cffunction> --->
|
|
|
|
|
|
</cfcomponent>
|