|
1 // Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies). |
|
2 // All rights reserved. |
|
3 // This component and the accompanying materials are made available |
|
4 // under the terms of "Eclipse Public License v1.0" |
|
5 // which accompanies this distribution, and is available |
|
6 // at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
7 // |
|
8 // Initial Contributors: |
|
9 // Nokia Corporation - initial contribution. |
|
10 // |
|
11 // Contributors: |
|
12 // |
|
13 // Description: |
|
14 // @internalComponent |
|
15 // |
|
16 // |
|
17 |
|
18 //System Includes |
|
19 #include <comms-infras/ss_nodeinterfaces.h> |
|
20 |
|
21 #include <httpstringconstants.h> |
|
22 #include <http/thttphdrval.h> |
|
23 #include <httperr.h> |
|
24 #include <http/thttptable.h> |
|
25 #include <upnp/tupnptable.h> |
|
26 |
|
27 //Local Includes |
|
28 #include "httpserverflow.h" |
|
29 #include "upnppint.h" |
|
30 #include "httpserver.h" |
|
31 #include "upnpserverconstants.h" |
|
32 #include "upnplog.h" |
|
33 #include "upnpmemoryutils.h" |
|
34 |
|
35 __FLOG_STMT(_LIT8(KComponent,"Flow");) |
|
36 using namespace ESock; |
|
37 |
|
38 CHttpServerFlow* CHttpServerFlow::NewL ( CSubConnectionFlowFactoryBase& aFactory, CProtocolIntfBase* aProtocolIntf, const TDesC8& aUri, const TNodeId& aSubConnId ) |
|
39 { |
|
40 CHttpServerFlow* self = new (ELeave) CHttpServerFlow( aFactory, aProtocolIntf, aSubConnId); |
|
41 CleanupStack::PushL ( self ); |
|
42 self->ConstructL ( aUri ); |
|
43 CleanupStack::Pop (); // self |
|
44 return self; |
|
45 } |
|
46 |
|
47 CHttpServerFlow::CHttpServerFlow(CSubConnectionFlowFactoryBase& aFactory, ESock::CProtocolIntfBase* aProtocolIntf, const TNodeId& aSubConnId ) |
|
48 : CUPnPFlowBase( aFactory, aProtocolIntf, EHttpServerFlow, aSubConnId ), |
|
49 iControlId ( 1 ) |
|
50 { |
|
51 LOG_NODE_CREATE ( KESockFlowTag, CHttpServerFlow ); |
|
52 } |
|
53 |
|
54 void CHttpServerFlow::ConstructL ( const TDesC8& aUri ) |
|
55 { |
|
56 iUri.CreateL ( aUri ); |
|
57 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("Created CHttpServerFlow"))); |
|
58 } |
|
59 |
|
60 CHttpServerFlow::~CHttpServerFlow () |
|
61 { |
|
62 iUri.Close (); |
|
63 iSubConnectionProvider.Close (); |
|
64 iControlTransactions.Reset(); |
|
65 iControlTransactions.Close (); |
|
66 |
|
67 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("Destroyed CHttpServerFlow"))); |
|
68 LOG_NODE_DESTROY(KESockFlowTag, CHttpServerFlow); |
|
69 } |
|
70 |
|
71 //From CSubConnectionFlowBase MNode |
|
72 void CHttpServerFlow::ReceivedL ( const TRuntimeCtxId& aSender, const TNodeId& /*aRecipient*/, TSignatureBase& aMessage ) |
|
73 { |
|
74 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ReceivedL"))); |
|
75 |
|
76 if ( aMessage.IsMessage<TEChild::TDestroy> () ) |
|
77 { |
|
78 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ReceivedL - Deleting current flow"))); |
|
79 if ( iSessionControlNotify ) |
|
80 { |
|
81 iSessionControlNotify->CanClose( MSessionControlNotify::EDelete ); |
|
82 } |
|
83 DeleteThisFlow (); |
|
84 } |
|
85 else if ( aMessage.IsMessage<TCFDataClient::TStart> () ) |
|
86 { |
|
87 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ReceivedL - TCFMessage::TDataClientStart"))); |
|
88 StartFlow ( address_cast < TNodeCtxId > ( aSender ) ); |
|
89 } |
|
90 else if ( aMessage.IsMessage<TCFDataClient::TStop> () ) |
|
91 { |
|
92 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ReceivedL - TCFMessage::TDataClientStop"))); |
|
93 StopFlow ( address_cast < TNodeCtxId > ( aSender ) ); |
|
94 } |
|
95 else if ( aMessage.IsMessage<TUpnpMessage::TUPnPResponseInfo> () ) |
|
96 { |
|
97 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ReceivedL - TUpnpMessage::TUPnPResponseInfo"))); |
|
98 const TUpnpMessage::TUPnPResponseInfo& msg = message_cast < TUpnpMessage::TUPnPResponseInfo > ( aMessage ); |
|
99 CServerTransaction* trans = static_cast < CServerTransaction* > ( msg.iTrans ); |
|
100 RMBufChain dataChain; |
|
101 TUPnPMemoryUtils::CreateMBuf(dataChain, trans->MemParts()); |
|
102 trans->FreeMemChunk(); |
|
103 trans->AddBodyPart(dataChain); |
|
104 HandleUPnPResponseL ( trans, msg.iStatus, msg.iInfo ); |
|
105 } |
|
106 } |
|
107 |
|
108 |
|
109 MFlowBinderControl* CHttpServerFlow::DoGetBinderControlL() |
|
110 { |
|
111 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::DoGetBinderControlL"))); |
|
112 return this; |
|
113 } |
|
114 |
|
115 // MFlowBinderControl |
|
116 CSubConnectionFlowBase* CHttpServerFlow::Flow() |
|
117 { |
|
118 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::Flow"))); |
|
119 return this; |
|
120 } |
|
121 |
|
122 MSessionControl* CHttpServerFlow::GetControlL (TInt /*aSessionType*/, MSessionControlNotify& aSessionControlNotify) |
|
123 { |
|
124 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::GetControlL"))); |
|
125 ASSERT(iSessionControlNotify == NULL); |
|
126 iSessionControlNotify = &aSessionControlNotify; |
|
127 |
|
128 //As of now UPnP Flow doesn't care more abt SessionType, since i'm caring abt only CSocket Binding, |
|
129 //May be for RInternalSocket binding this needs to be iterated. |
|
130 |
|
131 return this; |
|
132 } |
|
133 |
|
134 MSessionData* CHttpServerFlow::BindL(MSessionDataNotify& aNotify) |
|
135 { |
|
136 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::BindL"))); |
|
137 ASSERT(iSessionDataNotify == NULL); |
|
138 iSessionDataNotify = &aNotify; |
|
139 iSubConnectionProvider.PostMessage ( Id (), TCFControlProvider::TActive ().CRef () ); |
|
140 return this; |
|
141 } |
|
142 |
|
143 void CHttpServerFlow::Unbind() |
|
144 { |
|
145 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::Unbind"))); |
|
146 iSessionControlNotify = NULL; |
|
147 iSessionDataNotify = NULL; |
|
148 } |
|
149 |
|
150 // MSessionControl |
|
151 void CHttpServerFlow::Shutdown(MSessionControl::TCloseType aOption) |
|
152 { |
|
153 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::Shutdown"))); |
|
154 |
|
155 if ( MSessionControl::ENormal == aOption ) |
|
156 { |
|
157 // error the pending control transactions |
|
158 TUint count = iControlTransactions.Count (); |
|
159 |
|
160 for ( TInt pos = 0; pos < count; pos++ ) |
|
161 { |
|
162 CServerTransaction& trans = iControlTransactions[pos].iTransaction; |
|
163 if ( !trans.ReadyToSend () ) |
|
164 { |
|
165 // If we are not in ready to send state, it means that, we haven't created the response yet. |
|
166 CHttpServerHandler::CreateResponse ( *( trans.Response() ), HTTPStatus::EInternalServerError, &trans.ServerObserver() ); |
|
167 trans.SetReadyToSend (); |
|
168 THTTPEvent evt ( THTTPEvent::EGotResponseHeaders ); |
|
169 trans.ServerObserver ().OnHttpEvent ( &trans, evt ); |
|
170 } |
|
171 else if ( trans.CloseNeeded () ) |
|
172 { |
|
173 THTTPEvent evt ( THTTPEvent::EFailed ); |
|
174 trans.ServerObserver ().OnHttpEvent ( &trans, evt ); |
|
175 } |
|
176 } |
|
177 |
|
178 // remove control uri from http server, in order not to service further requests |
|
179 TNodeCtxId serviceId ( MeshMachine::KActivityNull, NodeId () ); |
|
180 TNodeCtxId controlProviderId ( MeshMachine::KActivityNull, TNodeId::NullId () ); |
|
181 static_cast<CUPnPProtocolIntfBase *>( ProtocolIntf () )->RemoveServiceUri ( iUri, serviceId, controlProviderId ); |
|
182 |
|
183 // send data client idle to scpr |
|
184 iSubConnectionProvider.PostMessage ( Id (), TCFControlProvider::TIdle ().CRef () ); |
|
185 } |
|
186 } |
|
187 |
|
188 void CHttpServerFlow::ActiveOpen() |
|
189 { |
|
190 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ActiveOpen"))); |
|
191 iSessionControlNotify->ConnectComplete(); |
|
192 } |
|
193 |
|
194 TInt CHttpServerFlow::GetOption(TUint aLevel, TUint aName, TDes8& anOption) const |
|
195 { |
|
196 if ( aLevel == KCHOptionLevel ) |
|
197 { |
|
198 |
|
199 TControlTransaction trans ( FirstControlTransactionToNotify () ); |
|
200 switch ( aName ) |
|
201 { |
|
202 case KCHMaxLength: |
|
203 { |
|
204 THTTPHdrVal value; |
|
205 if ( GetHeaderValue ( *(trans.iTransaction.Request()), HTTP::EContentLength, value, THTTPTable::Table() ) == KErrNotFound ) |
|
206 { |
|
207 *(TInt*) anOption.Ptr () = KErrUnknown; |
|
208 } |
|
209 else |
|
210 { |
|
211 *(TInt*) anOption.Ptr () = value.Int (); |
|
212 } |
|
213 } |
|
214 break; |
|
215 |
|
216 case KCHLastMessage: |
|
217 { |
|
218 *(TUint*) anOption.Ptr() = ( trans.iReadComplete && trans.iTransaction.BodyParts ().Length () == 0 ); |
|
219 } |
|
220 break; |
|
221 |
|
222 default: |
|
223 ASSERT(0); |
|
224 } |
|
225 } |
|
226 return 0; |
|
227 } |
|
228 |
|
229 TInt CHttpServerFlow::SetOption(TUint level, TUint name, const TDesC8& anOption) |
|
230 { |
|
231 TInt err = KErrNone; |
|
232 if ( level == KCHOptionLevel ) |
|
233 { |
|
234 TPckgBuf <TCHMessageOption> option; |
|
235 option.Copy ( anOption ); |
|
236 TInt pos = FindControlTransaction ( option().iId ); |
|
237 if ( pos == KErrNotFound ) |
|
238 return pos; |
|
239 TControlTransaction trans ( iControlTransactions[pos] ); |
|
240 switch ( name ) |
|
241 { |
|
242 case KCHMaxLength: |
|
243 { |
|
244 TRAP ( err, SetHeaderL ( *( trans.iTransaction.Response() ), HTTP::EContentLength, option().iValue, THTTPTable::Table() ) ); |
|
245 |
|
246 if ( err == KErrNone ) |
|
247 { |
|
248 // This will set the basic headers |
|
249 CHttpServerHandler::CreateResponse( *( trans.iTransaction.Response() ), HTTPStatus::EOk, &trans.iTransaction.ServerObserver() ); |
|
250 } |
|
251 else |
|
252 { |
|
253 CHttpServerHandler::CreateResponse( *( trans.iTransaction.Response() ), HTTPStatus::EInternalServerError, &trans.iTransaction.ServerObserver() ); |
|
254 } |
|
255 |
|
256 // Set the content length. |
|
257 trans.iTransaction.SetDataLeft ( option().iValue ); |
|
258 // commented to solve race issue... |
|
259 //trans.iTransaction.SetReadyToSend (); // Move to the ready to send state. When the next write happens |
|
260 // we just say body data is available |
|
261 //THTTPEvent evt ( THTTPEvent::EGotResponseHeaders ); |
|
262 //trans.iTransaction.ServerObserver().OnHttpEvent ( &trans.iTransaction, evt ); |
|
263 } |
|
264 break; |
|
265 |
|
266 case KCHLastMessage: |
|
267 { |
|
268 trans.iTransaction.SetComplete(); |
|
269 THTTPEvent evt ( THTTPEvent::EResponseComplete ); |
|
270 trans.iTransaction.ServerObserver().OnHttpEvent ( &trans.iTransaction, evt ); |
|
271 } |
|
272 break; |
|
273 |
|
274 default: |
|
275 ASSERT(0); |
|
276 } |
|
277 } |
|
278 return err; |
|
279 } |
|
280 |
|
281 // MSessionData |
|
282 |
|
283 TInt CHttpServerFlow::Write ( RMBufChain& aData, TUint aOptions, TSockAddr* /* anAddr */ ) |
|
284 { |
|
285 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::Write"))); |
|
286 TInt pos = FindControlTransaction ( aOptions ); |
|
287 TInt ret = KErrNone; |
|
288 if ( pos == KErrNotFound ) |
|
289 return pos; |
|
290 |
|
291 TInt len = aData.Length (); |
|
292 CServerTransaction& trans = iControlTransactions[pos].iTransaction; |
|
293 trans.AddBodyPart( aData ); |
|
294 |
|
295 if ( !trans.ReadyToSend() ) |
|
296 { |
|
297 // If we are not in ready to send state, it means that, we haven't created the response yet. |
|
298 trans.SetReadyToSend (); |
|
299 THTTPEvent evt ( THTTPEvent::EGotResponseHeaders ); |
|
300 ret = trans.ServerObserver().OnHttpEvent ( &trans, evt ); |
|
301 } |
|
302 else |
|
303 { |
|
304 THTTPEvent evt ( THTTPEvent::EGotResponseBodyData ); |
|
305 ret = trans.ServerObserver().OnHttpEvent ( &trans, evt ); |
|
306 } |
|
307 if(ret != KErrNone) |
|
308 { |
|
309 RemoveControlTransaction ( iControlTransactions[pos].iId ); |
|
310 //Ownership is transferred to trans Object. |
|
311 aData.Init (); |
|
312 return ret; |
|
313 } |
|
314 |
|
315 // remove control transaction object if complete data is received from socket |
|
316 if ( !trans.CloseNeeded () ) // closed needed informs that complete data is not yet received |
|
317 { |
|
318 RemoveControlTransaction ( iControlTransactions[pos].iId ); |
|
319 } |
|
320 |
|
321 aData.Init (); |
|
322 return len; |
|
323 } |
|
324 |
|
325 TInt CHttpServerFlow::GetData ( RMBufChain& aData, TUint aLength, TUint /*aOptions*/, TSockAddr* /*anAddr*/ ) |
|
326 { |
|
327 __ASSERT_DEBUG ( iControlTransactions.Count(), User::Invariant() ); |
|
328 CServerTransaction& trans = FirstControlTransactionToNotify ().iTransaction; |
|
329 |
|
330 RMBufChain newChain; |
|
331 RMBufChain& bodyChain = trans.BodyParts (); |
|
332 TInt err = bodyChain.Split ( aLength, newChain ); |
|
333 if ( err != KErrNone ) |
|
334 { |
|
335 // Our request validation failed. Generate the response |
|
336 CHttpServerHandler::CreateResponse ( *( trans.Response () ), err, &trans.ServerObserver() ); |
|
337 THTTPEvent evt ( THTTPEvent::ECompleteResponse ); |
|
338 trans.ServerObserver().OnHttpEvent ( &trans, evt ); |
|
339 } |
|
340 |
|
341 aData.Assign ( bodyChain ); |
|
342 bodyChain = newChain; |
|
343 |
|
344 return aData.Length(); |
|
345 } |
|
346 |
|
347 // From MHttpEventObserver |
|
348 TInt CHttpServerFlow::OnHttpEvent ( CTransaction* aTransaction, THTTPEvent& aEvent ) |
|
349 { |
|
350 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::OnHttpEvent"))); |
|
351 TInt ret = KErrNone; |
|
352 |
|
353 switch ( aEvent.iStatus ) |
|
354 { |
|
355 case THTTPEvent::EGotRequestHeaders: |
|
356 { |
|
357 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::OnHttpEvent - THTTPEvent::EGotRequestHeaders"))); |
|
358 ret = ValidateUPnPRequest( *aTransaction ); |
|
359 } |
|
360 break; |
|
361 |
|
362 default: |
|
363 ret = RouteTransaction ( static_cast < CServerTransaction& > (*aTransaction), aEvent ); |
|
364 break; |
|
365 } |
|
366 |
|
367 if ( ret != KErrNone ) |
|
368 { |
|
369 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::OnHttpEvent - THTTPEvent::EGotRequestHeaders - Creating response"))); |
|
370 // Our request validation failed. Generate the response |
|
371 CHttpServerHandler::CreateResponse ( *( aTransaction->Response () ), ret, &(static_cast < CServerTransaction& > (*aTransaction).ServerObserver()) ); |
|
372 ( static_cast < CServerTransaction* > ( aTransaction ) )->SetComplete (); |
|
373 ( static_cast < CServerTransaction* > ( aTransaction ) )->SetReadyToSend (); |
|
374 } |
|
375 return KErrNone; |
|
376 } |
|
377 |
|
378 void CHttpServerFlow::StartFlow ( TNodeCtxId aSender ) |
|
379 { |
|
380 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::StartFlow"))); |
|
381 |
|
382 TNodeCtxId serviceId ( MeshMachine::KActivityNull, NodeId () ); |
|
383 TRAPD ( err, static_cast<CUPnPProtocolIntfBase *>( ProtocolIntf() )->AddServiceUriL( iUri, *this, serviceId, aSender ) ); |
|
384 |
|
385 if ( err != KErrNone ) |
|
386 iSubConnectionProvider.PostMessage ( Id (), TEBase::TError ( TCFDataClient::TStart::Id (), err ).CRef() ); |
|
387 } |
|
388 |
|
389 void CHttpServerFlow::StopFlow ( TNodeCtxId aSender ) |
|
390 { |
|
391 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::StopFlow"))); |
|
392 |
|
393 TNodeCtxId serviceId ( MeshMachine::KActivityNull, NodeId () ); |
|
394 __ASSERT_DEBUG ( iSubConnectionProvider.RecipientId () == aSender, User::Invariant () ); |
|
395 |
|
396 static_cast<CUPnPProtocolIntfBase *>( ProtocolIntf () )->RemoveServiceUri ( iUri, serviceId, aSender ); |
|
397 } |
|
398 |
|
399 TInt CHttpServerFlow::RouteTransaction ( CServerTransaction& aTrans, THTTPEvent& aEvent ) |
|
400 { |
|
401 // ... Support chunked-encoding here for the request body data incase of POST & MPOST. |
|
402 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::RouteTransaction"))); |
|
403 TBool dataUp = EFalse; |
|
404 |
|
405 RStringF post = aTrans.Request()->Method().Pool().StringF(HTTP::EPOST, THTTPTable::Table()); |
|
406 RStringF mPost = aTrans.Request()->Method().Pool().StringF(UPnP::EMPost, TUPnPTable::Table()); |
|
407 |
|
408 if((aTrans.Request()->Method() == post) || (aTrans.Request()->Method() == mPost)) |
|
409 { |
|
410 dataUp = ETrue; |
|
411 } |
|
412 |
|
413 switch ( aEvent.iStatus ) |
|
414 { |
|
415 case THTTPEvent::EGotRequestBodyData: |
|
416 { |
|
417 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::RouteTransaction - THTTPEvent::EGotRequestBodyData"))); |
|
418 MHTTPDataSupplier* bodySupplier = aTrans.Request( )->Body( ); |
|
419 ASSERT(bodySupplier); |
|
420 |
|
421 TPtrC8 bodyPtr; |
|
422 RMBufChain bodyChain; |
|
423 bodySupplier->GetNextDataPart ( bodyPtr ); |
|
424 bodyChain.Create ( bodyPtr ); |
|
425 aTrans.AddBodyPart( bodyChain ); |
|
426 bodySupplier->ReleaseData( ); |
|
427 |
|
428 if ( dataUp ) |
|
429 { |
|
430 TInt index = FindOrCreateControlTransaction ( aTrans ); |
|
431 if ( index < KErrNotFound ) |
|
432 { |
|
433 return HTTPStatus::EInternalServerError; |
|
434 } |
|
435 |
|
436 TInt notifyDataLen = 0; |
|
437 if ( !iControlTransactions[index].iReadTriggered ) |
|
438 { |
|
439 notifyDataLen = sizeof ( TInt ); |
|
440 iControlTransactions[index].iReadTriggered = ETrue; |
|
441 } |
|
442 |
|
443 iSessionDataNotify->NewData ( notifyDataLen + bodyPtr.Length () ); |
|
444 } |
|
445 } |
|
446 break; |
|
447 case THTTPEvent::ERequestComplete: |
|
448 { |
|
449 if ( !dataUp ) |
|
450 { |
|
451 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::RouteTransaction - THTTPEvent::ERequestComplete"))); |
|
452 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::RouteTransaction - THTTPEvent::ERequestComplete - Posting message TUpnpMessage::TUPnPRequestInfo"))); |
|
453 |
|
454 IterateHeaders ( *aTrans.Request() ); |
|
455 // Post the response to the SCPR |
|
456 iSubConnectionProvider.PostMessage ( Id (), TUpnpMessage::TUPnPRequestInfo ( &aTrans ).CRef () ); |
|
457 } |
|
458 else |
|
459 { |
|
460 TInt index = FindOrCreateControlTransaction ( aTrans ); |
|
461 iControlTransactions[index].iReadComplete = ETrue; |
|
462 // make sure if receive is pending, ASocket will complete it |
|
463 iSessionDataNotify->NewData ( 0 ); |
|
464 } |
|
465 } |
|
466 break; |
|
467 |
|
468 default: |
|
469 ASSERT(0); |
|
470 break; |
|
471 } |
|
472 return KErrNone; |
|
473 } |
|
474 |
|
475 TInt CHttpServerFlow::GetHeaderValue ( const CRequest& aRequest, TInt aFieldIndex, THTTPHdrVal& aFieldVal, const TStringTable& aTable ) const |
|
476 { |
|
477 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::GetHeaderValue"))); |
|
478 CRequest& req = const_cast < CRequest& > ( aRequest ); |
|
479 RRequest request = req.Handle (); |
|
480 RHTTPHeaders headers = request.GetHeaderCollection (); |
|
481 RStringF fieldStr = aRequest.StringPool ().StringF ( aFieldIndex, aTable ); |
|
482 return headers.GetField ( fieldStr, 0, aFieldVal ); |
|
483 } |
|
484 |
|
485 TBool CHttpServerFlow::MatchHeaderValue ( const CRequest& aRequest, TInt aFieldIndex, const TDesC8& aFieldValue, const TStringTable& aTable ) |
|
486 { |
|
487 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::MatchHeaderValue"))); |
|
488 THTTPHdrVal value; |
|
489 if ( GetHeaderValue ( aRequest, aFieldIndex, value, aTable ) == KErrNone ) |
|
490 { |
|
491 RStringF valStr = value.StrF(); |
|
492 if ( valStr.DesC ().Compare ( aFieldValue ) == 0 ) |
|
493 { |
|
494 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::MatchHeaderValue - Returned ETrue"))); |
|
495 return ETrue; |
|
496 } |
|
497 } |
|
498 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::MatchHeaderValue - Returned EFalse"))); |
|
499 return EFalse; |
|
500 } |
|
501 |
|
502 TBool CHttpServerFlow::IsHeaderPresent ( const CRequest& aRequest, TInt aFieldIndex, const TStringTable& aTable ) |
|
503 { |
|
504 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::IsHeaderPresent"))); |
|
505 THTTPHdrVal value; |
|
506 return GetHeaderValue ( aRequest, aFieldIndex, value, aTable ) == KErrNone; |
|
507 } |
|
508 |
|
509 TBool CHttpServerFlow::IsValidNTHeader ( const CRequest& aRequest ) |
|
510 { |
|
511 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::IsValidNTHeader"))); |
|
512 _LIT8 ( KNTValue, "upnp:event" ); |
|
513 return MatchHeaderValue ( aRequest, UPnP::ENT, KNTValue(), TUPnPTable::Table() ); |
|
514 } |
|
515 |
|
516 TBool CHttpServerFlow::IsValidContentTypeHeader ( const CRequest& aRequest ) |
|
517 { |
|
518 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::IsValidContentTypeHeader"))); |
|
519 _LIT8 ( KContentTypeValue, "text/xml" ); |
|
520 return MatchHeaderValue ( aRequest, HTTP::EContentType, KContentTypeValue(), THTTPTable::Table() ); |
|
521 } |
|
522 |
|
523 TBool CHttpServerFlow::IsValidNTSHeader ( const CRequest& aRequest ) |
|
524 { |
|
525 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::IsValidNTSHeader"))); |
|
526 _LIT8 ( KNTSValue, "upnp:propchange" ); |
|
527 return MatchHeaderValue ( aRequest, UPnP::ENTS, KNTSValue(), TUPnPTable::Table() ); |
|
528 } |
|
529 |
|
530 TBool CHttpServerFlow::IsValidManHeader ( const CRequest& aRequest ) |
|
531 { |
|
532 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::IsValidManHeader"))); |
|
533 _LIT8 ( KManValue, "http://schemas.xmlsoap.org/soap/envelope/"); |
|
534 return MatchHeaderValue ( aRequest, UPnP::EMAN, KManValue(), TUPnPTable::Table() ); |
|
535 } |
|
536 |
|
537 void CHttpServerFlow::SetHeaderL ( CResponse& aRequest, TInt aFieldIndex, RMemChunk& aVal, const TStringTable& aTable ) |
|
538 { |
|
539 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::SetHeaderL"))); |
|
540 RStringPool sp = aRequest.StringPool (); |
|
541 RHTTPHeaders hdr = aRequest.Handle ().GetHeaderCollection (); |
|
542 TPtrC8 value; |
|
543 value.Set ( aVal.First()->Ptr (), aVal.Length () ); |
|
544 RStringF valStr = sp.OpenFStringL ( value ); |
|
545 THTTPHdrVal hdrVal ( valStr ); |
|
546 hdr.SetFieldL ( sp.StringF(aFieldIndex, aTable ), hdrVal ); |
|
547 valStr.Close (); |
|
548 } |
|
549 |
|
550 void CHttpServerFlow::SetHeaderL ( CResponse& aRequest, TInt aFieldIndex, TInt aVal, const TStringTable& aTable ) |
|
551 { |
|
552 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::SetHeaderL"))); |
|
553 RStringPool sp = aRequest.StringPool (); |
|
554 RHTTPHeaders hdr = aRequest.Handle ().GetHeaderCollection (); |
|
555 THTTPHdrVal hdrVal ( aVal ); |
|
556 hdr.SetFieldL ( sp.StringF(aFieldIndex, aTable ), hdrVal ); |
|
557 } |
|
558 |
|
559 |
|
560 // Returns KErrNone if validation is suceeded otherwise one of the HTTP error status code will be returned |
|
561 TInt CHttpServerFlow::ValidateUPnPRequest ( const CTransaction& aTrans ) |
|
562 { |
|
563 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest"))); |
|
564 |
|
565 RStringF get = aTrans.Request()->Method().Pool().StringF(HTTP::EGET, THTTPTable::Table()); |
|
566 RStringF post = aTrans.Request()->Method().Pool().StringF(HTTP::EPOST, THTTPTable::Table()); |
|
567 TInt method = KErrNotFound; |
|
568 TInt ret = KErrNone; |
|
569 |
|
570 if((aTrans.Request()->Method() == get)||(aTrans.Request()->Method() == post)) |
|
571 { |
|
572 method = aTrans.Request()->Method().Index(THTTPTable::Table()); |
|
573 switch ( method ) |
|
574 { |
|
575 case HTTP::EGET: |
|
576 break; |
|
577 |
|
578 case HTTP::EPOST: |
|
579 { |
|
580 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - Validating POST request"))); |
|
581 ret = ValidatePostRequest ( aTrans ); |
|
582 } |
|
583 break; |
|
584 |
|
585 default: |
|
586 { |
|
587 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - HTTPStatus::ENotImplemented"))); |
|
588 ret = HTTPStatus::ENotImplemented; |
|
589 } |
|
590 break; |
|
591 } |
|
592 } |
|
593 |
|
594 else |
|
595 { |
|
596 method = aTrans.Request()->Method().Index(TUPnPTable::Table()); |
|
597 switch ( method ) |
|
598 { |
|
599 case UPnP::EMPost: |
|
600 { |
|
601 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - Validating MPOST request"))); |
|
602 ret = ValidateMPostRequest ( aTrans ); |
|
603 } |
|
604 break; |
|
605 |
|
606 case UPnP::ESubscribe: |
|
607 { |
|
608 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - Validating SUBSCRIBE request"))); |
|
609 ret = ValidateSubscribeRequest ( aTrans ); |
|
610 } |
|
611 break; |
|
612 |
|
613 case UPnP::EUnsubscribe: |
|
614 { |
|
615 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - Validating UNSUBSCRIBE request"))); |
|
616 ret = ValidateUnsubscribeRequest ( aTrans ); |
|
617 } |
|
618 break; |
|
619 |
|
620 case UPnP::ENotify: |
|
621 { |
|
622 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - Validating NOTIFY request"))); |
|
623 ret = ValidateNotifyRequest ( aTrans ); |
|
624 } |
|
625 break; |
|
626 |
|
627 default: |
|
628 { |
|
629 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - HTTPStatus::ENotImplemented"))); |
|
630 ret = HTTPStatus::ENotImplemented; |
|
631 } |
|
632 break; |
|
633 } |
|
634 } |
|
635 |
|
636 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUPnPRequest - Returned with error code = %d"), ret)); |
|
637 return ret; |
|
638 } |
|
639 |
|
640 // Returns KErrNone if validation is suceeded otherwise one of the HTTP error status code will be returned |
|
641 TInt CHttpServerFlow::ValidateSubscribeRequest ( const CTransaction& aTrans ) |
|
642 { |
|
643 // General: SUBSCRIBE request MUST not contain a body |
|
644 // 1. Subscribing: SUBSCRIBE request MUST contain a CALLBACK, NT headers. The NT header valus MUST be |
|
645 // "upnp:event". |
|
646 // Response: 412 status code ( pre-condition failed) - If NT or CALLBACK request header is not present |
|
647 // 400 Bad request - If SID header and one of NT or CALLBACK headers are present |
|
648 // 2. Re-subscribing: SUBSCRIBE request MUST contain a SID header and MUST not contain |
|
649 // a CALLBACK or NT header. |
|
650 // Response: Same as 1 except, if the SID is not valid we respond with 412 ( pre-condition failed ) |
|
651 // The 412 error condition is handled in this case by the SCPR. Here we will not check whether the |
|
652 // SID is valid or not |
|
653 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateSubscribeRequest"))); |
|
654 CRequest& request = *( aTrans.Request () ); |
|
655 |
|
656 if ( request.Handle ().HasBody () ) |
|
657 return HTTPStatus::EBadRequest; // MUST not contain a body |
|
658 |
|
659 TBool callback = IsHeaderPresent ( request, UPnP::ECallback, TUPnPTable::Table() ); |
|
660 TBool nt = IsHeaderPresent ( request, UPnP::ENT, TUPnPTable::Table() ); |
|
661 TBool sid = IsHeaderPresent ( request, UPnP::ESID, TUPnPTable::Table() ); |
|
662 |
|
663 if ( callback && nt ) |
|
664 { |
|
665 // Subscription request |
|
666 // Check the NT header value == MUST be "upnp:event" |
|
667 if ( IsValidNTHeader ( request ) ) |
|
668 return sid ? HTTPStatus::EBadRequest : KErrNone; |
|
669 } |
|
670 if ( sid ) |
|
671 { |
|
672 // Re-subscribe request |
|
673 return ( callback || nt ) ? HTTPStatus::EBadRequest : KErrNone; |
|
674 } |
|
675 |
|
676 // Otherwise pre-condition failed |
|
677 return HTTPStatus::EPreconditionFailed; |
|
678 } |
|
679 |
|
680 // Returns KErrNone if validation is suceeded otherwise one of the HTTP error status code will be returned |
|
681 TInt CHttpServerFlow::ValidateUnsubscribeRequest ( const CTransaction& aTrans ) |
|
682 { |
|
683 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateUnsubscribeRequest"))); |
|
684 // General: UNSUBSCRIBE request MUST not contain a body |
|
685 CRequest& request = *( aTrans.Request () ); |
|
686 |
|
687 if ( request.Handle ().HasBody () ) |
|
688 return HTTPStatus::EBadRequest; // MUST not contain a body |
|
689 |
|
690 TBool callback = IsHeaderPresent ( request, UPnP::ECallback, TUPnPTable::Table() ); |
|
691 TBool nt = IsHeaderPresent ( request, UPnP::ENT, TUPnPTable::Table() ); |
|
692 TBool sid = IsHeaderPresent ( request, UPnP::ESID, TUPnPTable::Table() ); |
|
693 if ( sid ) |
|
694 { |
|
695 return ( callback || nt ) ? HTTPStatus::EBadRequest : KErrNone; |
|
696 } |
|
697 // Otherwise pre-condition failed |
|
698 return HTTPStatus::EPreconditionFailed; |
|
699 } |
|
700 |
|
701 // Returns KErrNone if validation is suceeded otherwise one of the HTTP error status code will be returned |
|
702 TInt CHttpServerFlow::ValidateNotifyRequest ( const CTransaction& aTrans ) |
|
703 { |
|
704 // NOTIFY request |
|
705 // 1. Content-Type header value MUST be "text/xml" |
|
706 // 2. NT header value MUST be "upnp:event" |
|
707 // 3. NTS header value MUST be "upnp:propchange" |
|
708 // 4. SID header value MUST be present. ( we check only the presence here ) |
|
709 // 5. SEQ header value MUST be present ( we check only the presence here ) |
|
710 // 6. MUST contain a body |
|
711 // Errors: |
|
712 // Bad request - If NT or NTS header is missing |
|
713 // Pre-condition failed - If the above 1-5 condition is failed |
|
714 |
|
715 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::ValidateNotifyRequest"))); |
|
716 CRequest& request = *( aTrans.Request () ); |
|
717 |
|
718 if ( IsValidContentTypeHeader ( request ) ) |
|
719 { |
|
720 if ( IsValidNTHeader ( request ) && IsValidNTSHeader ( request ) ) |
|
721 { |
|
722 if ( IsHeaderPresent ( request, UPnP::ESID, TUPnPTable::Table() ) && IsHeaderPresent ( request, UPnP::ESEQ, TUPnPTable::Table() ) ) |
|
723 { |
|
724 return KErrNone; |
|
725 } |
|
726 } |
|
727 else if ( !IsHeaderPresent ( request, UPnP::ENT, TUPnPTable::Table() ) || !IsHeaderPresent ( request, UPnP::ENTS, TUPnPTable::Table() ) ) |
|
728 { |
|
729 return HTTPStatus::EBadRequest; |
|
730 } |
|
731 } |
|
732 // Otherwise pre-condition failed |
|
733 return HTTPStatus::EPreconditionFailed; |
|
734 } |
|
735 |
|
736 // Returns KErrNone if validation is suceeded otherwise one of the HTTP error status code will be returned |
|
737 TInt CHttpServerFlow::ValidatePostRequest ( const CTransaction& aTrans ) |
|
738 { |
|
739 // 1. Content-Type header MUST contain text/xml value -- On failure responds with EUnsupportedMediaType status code |
|
740 // 2. Content-Type header MUST contain charset as utf-8 --- On failure responds with EBadRequest status code |
|
741 // 3. SOAPACTION header field MUST be present -- On failure responds with EPreconditionFailed status code |
|
742 // 4. POST request MUST contain a body -- On failure responds with EBadRequest |
|
743 // 5. Currently we support only the POST method with Content-Length header on the request. If it is not present |
|
744 // we return Not Implemented ( 501 ) error response. In the future it may need implementation. |
|
745 |
|
746 CRequest& request = *( aTrans.Request () ); |
|
747 |
|
748 if ( !IsHeaderPresent ( request, HTTP::EContentLength, THTTPTable::Table() ) ) |
|
749 return HTTPStatus::ENotImplemented; |
|
750 |
|
751 if ( IsValidContentTypeHeader ( request ) && IsValidCharset ( request ) ) |
|
752 { |
|
753 if ( IsHeaderPresent ( request , UPnP::ESoapAction, TUPnPTable::Table() ) ) |
|
754 { |
|
755 return KErrNone; |
|
756 } |
|
757 else |
|
758 { |
|
759 return HTTPStatus::EPreconditionFailed; |
|
760 } |
|
761 } |
|
762 return HTTPStatus::EUnsupportedMediaType; |
|
763 } |
|
764 |
|
765 // Returns KErrNone if validation is suceeded otherwise one of the HTTP error status code will be returned |
|
766 TInt CHttpServerFlow::ValidateMPostRequest ( const CTransaction& aTrans ) |
|
767 { |
|
768 // 1. MAN header must be present with value "http://schemas.xmlsoap,org/soap/envelope/" |
|
769 // On failure responds with KEBadRequest |
|
770 // 2. Checks the namespace and that matches with ns-SOAPACTION... ?? |
|
771 // 3. Validate like the normal POST request |
|
772 CRequest& request = *( aTrans.Request () ); |
|
773 |
|
774 if ( IsHeaderPresent ( request, UPnP::EMAN, TUPnPTable::Table() ) && IsValidManHeader( request ) ) |
|
775 { |
|
776 return ValidatePostRequest ( aTrans ); |
|
777 } |
|
778 return HTTPStatus::EBadRequest; |
|
779 } |
|
780 |
|
781 void CHttpServerFlow::HandleUPnPResponseL ( CServerTransaction* aTrans, TInt aStatus, SMetaDataNetCtorExt* aInfo ) |
|
782 { |
|
783 LOG(ESockLogExternal::Printf(KSubsysHttpSrvrFlow, KComponent, _L8("CHttpServerFlow::HandleUPnPResponseL"))); |
|
784 CResponse* response = aTrans->Response (); |
|
785 |
|
786 CHttpServerHandler::CreateResponse ( *response, aStatus, &aTrans->ServerObserver () ); |
|
787 |
|
788 if ( aTrans->Request ()->Handle ().Method ().Index ( TUPnPTable::Table() ) == UPnP::ESubscribe && aStatus == HTTPStatus::EOk ) |
|
789 { |
|
790 TSubsribeResponseInfo& info = *( reinterpret_cast < TSubsribeResponseInfo* > ( aInfo ) ); |
|
791 SetHeaderL ( *response, UPnP::ESID, info.iSid, TUPnPTable::Table()); |
|
792 SetHeaderL ( *response, UPnP::ETimeout, info.iTimeout, TUPnPTable::Table() ); |
|
793 info.iSid.Free (); // Free the RMBufChain |
|
794 } |
|
795 |
|
796 // Set the Content-Length header |
|
797 SetHeaderL ( *response, HTTP::EContentLength, aTrans->BodyParts ().Length (), THTTPTable::Table() ); |
|
798 aTrans->SetDataLeft ( aTrans->BodyParts ().Length () ); |
|
799 aTrans->SetComplete (); |
|
800 |
|
801 THTTPEvent evt ( THTTPEvent::ECompleteResponse ); |
|
802 aTrans->ServerObserver().OnHttpEvent ( aTrans, evt ); |
|
803 } |
|
804 |
|
805 void CHttpServerFlow::IterateHeaders ( CRequest& aRequest ) |
|
806 { |
|
807 RHTTPHeaders hdr = aRequest.Handle ().GetHeaderCollection (); |
|
808 RStringPool sp = aRequest.StringPool (); |
|
809 THTTPHdrFieldIter it = hdr.Fields (); |
|
810 |
|
811 while ( it.AtEnd () == EFalse ) |
|
812 { |
|
813 RStringTokenF fieldName = it (); |
|
814 RStringF fieldNameStr = sp.StringF ( fieldName ); |
|
815 |
|
816 THTTPHdrVal fieldVal; |
|
817 hdr.GetField ( fieldNameStr, 0, fieldVal ); |
|
818 |
|
819 ++it; |
|
820 } |
|
821 } |
|
822 |
|
823 TInt CHttpServerFlow::FindOrCreateControlTransaction ( CServerTransaction& aTrans ) |
|
824 { |
|
825 TInt index = FindControlTransaction ( aTrans ); |
|
826 if ( index == KErrNotFound ) |
|
827 { |
|
828 return AddControlTransaction ( iControlId++, aTrans ); |
|
829 } |
|
830 return index; |
|
831 } |
|
832 |
|
833 |
|
834 TInt CHttpServerFlow::FindControlTransaction ( TInt aId ) const |
|
835 { |
|
836 for ( TInt i = 0; i < iControlTransactions.Count (); ++i ) |
|
837 { |
|
838 if ( iControlTransactions[i].iId == aId ) |
|
839 { |
|
840 return i; |
|
841 } |
|
842 } |
|
843 return KErrNotFound; |
|
844 } |
|
845 |
|
846 TInt CHttpServerFlow::FindControlTransaction ( CServerTransaction& aTrans ) const |
|
847 { |
|
848 for ( TInt i = 0; i < iControlTransactions.Count (); ++i ) |
|
849 { |
|
850 if ( &(iControlTransactions[i].iTransaction) == &aTrans ) |
|
851 { |
|
852 return i; |
|
853 } |
|
854 } |
|
855 return KErrNotFound; |
|
856 } |
|
857 |
|
858 void CHttpServerFlow::RemoveControlTransaction ( TInt aId ) |
|
859 { |
|
860 TInt pos = FindControlTransaction ( aId ); |
|
861 if ( pos != KErrNotFound ) |
|
862 { |
|
863 iControlTransactions.Remove ( pos ); |
|
864 } |
|
865 } |
|
866 |
|
867 TInt CHttpServerFlow::AddControlTransaction ( TInt aId, CServerTransaction& aTrans ) |
|
868 { |
|
869 TControlTransaction trans ( aId, aTrans ); |
|
870 TInt err = iControlTransactions.Append ( trans ); |
|
871 if ( err == KErrNone ) |
|
872 { |
|
873 const TUint KIdentifiersLen = sizeof ( TInt ); |
|
874 TBuf8<KIdentifiersLen> controlBuf; |
|
875 controlBuf.AppendNumFixedWidth ( aId, EDecimal, KIdentifiersLen ); |
|
876 |
|
877 RMBufChain bufChain; |
|
878 err = bufChain.Create ( controlBuf ); |
|
879 if ( err != KErrNone ) |
|
880 return err; |
|
881 aTrans.BodyParts().Prepend ( bufChain ); |
|
882 } |
|
883 |
|
884 return err == KErrNone ? iControlTransactions.Count() - 1 : err; |
|
885 } |
|
886 |
|
887 CHttpServerFlow::TControlTransaction CHttpServerFlow::FirstControlTransactionToNotify () const |
|
888 { |
|
889 TInt i = 0; |
|
890 for ( i = 0; i < iControlTransactions.Count (); ++i ) |
|
891 { |
|
892 if ( iControlTransactions[i].iNotifyComplete == EFalse ) |
|
893 { |
|
894 break; |
|
895 } |
|
896 } |
|
897 return iControlTransactions[i]; |
|
898 } |
|
899 |
|
900 |