TheXman Posted May 6, 2023 Author Share Posted May 6, 2023 (edited) I think you are headed in the right direction. Let me know how it goes or if I can help. If you come up with new or modified functionality that would benefit others, I would definitely consider adding to the HTTPAPI UDF lib. Edited May 6, 2023 by TheXman noellarkin, sylremo and argumentum 3 CryptoNG UDF: Cryptography API: Next Gen jq UDF: Powerful and Flexible JSON Processor | jqPlayground: An Interactive JSON Processor Xml2Json UDF: Transform XML to JSON | HttpApi UDF: HTTP Server API | Roku Remote: Example Script About Me How To Ask Good Questions On Technical And Scientific Forums (Detailed) | How to Ask Good Technical Questions (Brief) "Any fool can know. The point is to understand." -Albert Einstein "If you think you're a big fish, it's probably because you only swim in small ponds." ~TheXman Link to comment Share on other sites More sharing options...
noellarkin Posted August 15, 2023 Share Posted August 15, 2023 Been using this for a personal project, love it:) Just had a suggestion: In the function _HTTPAPI_HttpSendHttpResponse adding an additional parameter $sResponseType Func _HTTPAPI_HttpSendHttpResponse($hRequestQueue, $iRequestId, $iStatusCode, $sReason, $sBody, $sResponseType = "text/html") And replacnig "text/html" in the function with $sResponseType. Allows one to use other formats, like JSON :) For example: Switch $sPath Case $path & "/username" $iStatusCode = 200 $sReason = "OK" $sMsg = '{"username":"' & @UserName & '"}' EndSwitch _HTTPAPI_HttpSendHttpResponse($requestqueue, $tRequest.RequestId, $iStatusCode, $sReason, $sMsg, "application/json") Hashim and Musashi 2 Link to comment Share on other sites More sharing options...
TheXman Posted August 15, 2023 Author Share Posted August 15, 2023 Thank you @noellarkin, I'm glad you've found the UDF useful. I think your suggestion of being able to set the "Content-Type" response header to something other than "text/html" is an excellent one. Although a "Content-Type" of "text/html" would work for JSON and most other content types, I can definitely think of reasons why having that header set to something more specific would be beneficial on the client side. I will implement your feature request of being able to set the "Content-Type" header value in the call to _HTTPAPI_HttpSendHttpResponse(). Since it is now morning where I am, I should be able to post the updated version later today. Thanks Hashim 1 CryptoNG UDF: Cryptography API: Next Gen jq UDF: Powerful and Flexible JSON Processor | jqPlayground: An Interactive JSON Processor Xml2Json UDF: Transform XML to JSON | HttpApi UDF: HTTP Server API | Roku Remote: Example Script About Me How To Ask Good Questions On Technical And Scientific Forums (Detailed) | How to Ask Good Technical Questions (Brief) "Any fool can know. The point is to understand." -Albert Einstein "If you think you're a big fish, it's probably because you only swim in small ponds." ~TheXman Link to comment Share on other sites More sharing options...
TheXman Posted August 15, 2023 Author Share Posted August 15, 2023 (edited) What's New in Version v1.4.0 v1.4.0 (2023-08-15) Added the ability to set the Content-Type response header's value in _HTTPAPI_HttpSendHttpResponse(). See the function's header and the example script in order to see how to implement the new functionality. Thanks @noellarkin for the suggestion. Added an additional api link (/json) to the example script in order to show the new content-type functionality in _HTTPAPI_HttpSendHttpResponse(). v1.3.0 (2022-06-14) - These modifications were not previously released. Added 2 new internal functions related to setting response headers: __HTTPAPI_ResetKnownResponseHeaderValue() - Clears a known response header __HTTPAPI_SetKnownResponseHeaderValue() - Set a known response header Did a little code optimization/refactoring in _HTTPAPI_HttpSendHttpResponse() and some of the struct definitions. Edited August 15, 2023 by TheXman Hashim, noellarkin, Musashi and 1 other 4 CryptoNG UDF: Cryptography API: Next Gen jq UDF: Powerful and Flexible JSON Processor | jqPlayground: An Interactive JSON Processor Xml2Json UDF: Transform XML to JSON | HttpApi UDF: HTTP Server API | Roku Remote: Example Script About Me How To Ask Good Questions On Technical And Scientific Forums (Detailed) | How to Ask Good Technical Questions (Brief) "Any fool can know. The point is to understand." -Albert Einstein "If you think you're a big fish, it's probably because you only swim in small ponds." ~TheXman Link to comment Share on other sites More sharing options...
noellarkin Posted August 15, 2023 Share Posted August 15, 2023 Glad I could help :) I was working on this today and I wrote some helper functions for HTTP API as well, perhaps some of these will be useful. Helpers: _HTTPAPI_BindGroupIDRequestQueue, _HTTPAPI_UnbindGroupIDRequestQueue and _HTTPAPI_CaptureCmd are directly from your example script. _HTTPAPI_CreateRequestCaseArray and _HTTPAPI_AddRequestCase make it easier to add different cases, replacing the switch/case with a search function. _HTTPAPI_ProcessRequests does exactly that. _HTTPAPI_Run abstracts away everything but the essentials. These are the functions: expandcollapse popup#include-once Func _HTTPAPI_BindGroupIDRequestQueue($groupid, $requestqueue) Local $FunctionName = _DebugEX_StartFunc("_HTTPAPI_BindGroupIDRequestQueue") Local $ReturnValue = 0 Local $BindingInfo = DllStructCreate($__HTTPAPI_gtagHTTP_BINDING_INFO) $BindingInfo.Flags = $__HTTPAPI_HTTP_PROPERTY_FLAG_PRESENT $BindingInfo.RequestQueueHandle = $requestqueue Local $GroupPropertySet = _HTTPAPI_HttpSetUrlGroupProperty($groupid, $__HTPPAPI_HttpServerBindingProperty, DllStructGetPtr($BindingInfo), DllStructGetSize($BindingInfo)) If $GroupPropertySet = True Then $ReturnValue = 1 Else _DebugEX_ERROR($FunctionName, "GroupPropertySet:" & $GroupPropertySet) Endif Return $ReturnValue EndFunc Func _HTTPAPI_UnbindGroupIDRequestQueue($groupid) Local $FunctionName = _DebugEX_StartFunc("_HTTPAPI_UnbindGroupIDRequestQueue") Local $ReturnValue = 0 Local $BindingInfo = DllStructCreate($__HTTPAPI_gtagHTTP_BINDING_INFO) $BindingInfo.Flags = 0 $BindingInfo.RequestQueueHandle = Null Local $GroupPropertySet = _HTTPAPI_HttpSetUrlGroupProperty($groupid, $__HTPPAPI_HttpServerBindingProperty, DllStructGetPtr($BindingInfo), DllStructGetSize($BindingInfo)) If $GroupPropertySet = True Then $ReturnValue = 1 Else _DebugEX_ERROR($FunctionName, "GroupPropertySet:" & $GroupPropertySet) Endif Return $ReturnValue EndFunc Func _HTTPAPI_CaptureCmd($sCmd, $sWorkingDir = @WorkingDir) Local $iPID = 0 Local $sOutput = "" ;resolve defaults If $sWorkingDir = Default Then $sWorkingDir = @WorkingDir ;execute command $iPID = Run(@ComSpec & ' /c ' & $sCmd, $sWorkingDir, @SW_HIDE, $STDERR_MERGED) If @error Then ConsoleWrite("Error: Unable to execute command - " & $sCmd & @CRLF) Return SetError(1, 0, "") EndIf ;wait for process to end ProcessWaitClose($iPID) ;get output $sOutput = StdoutRead($iPID) ;if no output, set @error and return empty string If $sOutput = "" Then Return SetError(2, 0, "") ;return output Return $sOutput EndFunc Func _HTTPAPI_CreateRequestCaseArray() ; verb, path, statuscode, reason, messsage, responsetype, execute (0/1) Local $ReturnValue[0][7] Return $ReturnValue EndFunc Func _HTTPAPI_AddRequestCase(ByRef $thisrequestcasearray, $verb, $path, $statuscode, $reason, $message, $responsetype, $execute = 0) Local $FunctionName = _DebugEX_StartFunc("_HTTPAPI_AddCase") Local $Append[1][7] $Append[0][0] = String($verb) $Append[0][1] = String($path) $Append[0][2] = Int($statuscode) $Append[0][3] = String($reason) $Append[0][4] = String($message) $Append[0][5] = String($responsetype) $Append[0][6] = Int($execute) _ArrayConcatenate($thisrequestcasearray, $Append) Return 1 EndFunc Func _HTTPAPI_ProcessRequests(ByRef $thisrequestcasearray, $requestqueue) Local $FunctionName = _DebugEX_StartFunc("_HTTPAPI_ProcessRequests") Local $GETCase[0][6] Local $POSTCase[0][6] Local $Append[1][6] For $i = 0 To _ArrayEnd($thisrequestcasearray) $Append[0][0] = $thisrequestcasearray[$i][1] $Append[0][1] = $thisrequestcasearray[$i][2] $Append[0][2] = $thisrequestcasearray[$i][3] $Append[0][3] = $thisrequestcasearray[$i][4] $Append[0][4] = $thisrequestcasearray[$i][5] $Append[0][5] = $thisrequestcasearray[$i][6] Switch $thisrequestcasearray[$i][0] Case "GET" _ArrayConcatenate($GETCase, $Append) Case "POST" _ArrayConcatenate($POSTCase, $Append) EndSwitch Next Local $tBuffer = "" Local $tRequest = "" Local $sPath = "" Local $QueryString = "" Local $RequestBody = "" Local $SearchThisArray = "" Local $SearchResult = -1 Local $StatusCode = 0 Local $Reason = "" Local $Message = "" Local $ResponseType = "" Local $Execute = 0 Local $FunctionExecuteOutput = 0 While 1 $tBuffer = _HTTPAPI_HttpReceiveHttpRequest($requestqueue) $tRequest = DllStructCreate($__HTTPAPI_gtagHTTP_REQUEST_V2 & StringFormat("byte body[%i];", $__HTTPAPI_REQUEST_BODY_SIZE), DllStructGetPtr($tBuffer)) $sPath = _WinAPI_GetString($tRequest.pAbsPath) $QueryString = _WinAPI_GetString($tRequest.pQueryString) $RequestBody = "" If $tRequest.pEntityChunks > 0 Then $RequestBody = _HTTPAPI_GetRequestBody($tRequest) ConsoleWrite(@CRLF & "RequestBody:" & $RequestBody) EndIf If StringRight($sPath, 1) = "/" Then $sPath = StringTrimRight($sPath, 1) If _StringContainsSubstring($sPath, "/stop") Then _HTTPAPI_HttpSendHttpResponse($requestqueue, $tRequest.RequestId, 200, "OK", "Shutting down API...", "text/plain") ConsoleWrite(@CRLF & "Shutting down API...") ExitLoop Else If $tRequest.Verb = $__HTTPAPI_HttpVerbGET Then $SearchThisArray = $GETCase ConsoleWrite(@CRLF & "GET") EndIf If $tRequest.Verb = $__HTTPAPI_HttpVerbPOST Then $SearchThisArray = $POSTCase ConsoleWrite(@CRLF & "POST") EndIf $SearchResult = _ArraySearch($SearchThisArray, $sPath, 0, 0, 0, 2, Default, Default, Default) If _ArraySearchFound($SearchResult) Then ConsoleWrite(@CRLF & "found") $StatusCode = $SearchThisArray[$SearchResult][1] $Reason = $SearchThisArray[$SearchResult][2] $Message = $SearchThisArray[$SearchResult][3] $ResponseType = $SearchThisArray[$SearchResult][4] $Execute = $SearchThisArray[$SearchResult][5] ConsoleWrite(@CRLF & "StatusCode:" & $StatusCode) ConsoleWrite(@CRLF & "Reason:" & $Reason) ConsoleWrite(@CRLF & "Message:" & $Message) ConsoleWrite(@CRLF & "ResponseType:" & $ResponseType) ConsoleWrite(@CRLF & "Execute:" & $Execute) If $Execute Then $FunctionExecuteOutput = Execute($Message) $Message = $FunctionExecuteOutput EndIf _HTTPAPI_HttpSendHttpResponse($requestqueue, $tRequest.RequestId, $StatusCode, $Reason, $Message, $ResponseType) Else _HTTPAPI_HttpSendHttpResponse($requestqueue, $tRequest.RequestId, 404, "Not Found", "<h2>Not Found</h2><hr><p>HTTP Error 404. The requested resource is not found.</p>", "text/html") ConsoleWrite(@CRLF & "not found") EndIf $tBuffer = 0 Sleep(100) EndIf WEnd EndFunc Func _HTTPAPI_Run($host, $path, $requestcases) Local $FunctionName = _DebugEX_StartFunc("_HTTPAPI_Run") Local $ReturnValue = 1 Local $EnvironmentInitialized = _HTTPAPI_Startup(True) If $EnvironmentInitialized Then Local $ServerInitialized = _HTTPAPI_HttpInitialize() If $ServerInitialized Then Local $ServerSessionID = _HTTPAPI_HttpCreateServerSession() If $ServerSessionID <> 0 Then Local $URLGroupID = _HTTPAPI_HttpCreateUrlGroup($ServerSessionID) If $URLGroupID <> 0 Then Local $RequestQueue = _HTTPAPI_HttpCreateRequestQueue() If $RequestQueue <> 0 Then Local $GroupQueueBound = _HTTPAPI_BindGroupIDRequestQueue($URLGroupID, $RequestQueue) If $GroupQueueBound Then Local $BaseURLAdded = _HTTPAPI_HttpAddUrlToUrlGroup($URLGroupID, $host & $path) If $BaseURLAdded Then _HTTPAPI_ProcessRequests($requestcases, $RequestQueue) OnAutoItExitRegister(_HTTPAPI_HttpRemoveUrlFromUrlGroup($URLGroupID, "", $__HTTPAPI_HTTP_URL_FLAG_REMOVE_ALL)) Else _DebugEX_ERROR($FunctionName, "BaseURLAdded:" & $BaseURLAdded) EndIf OnAutoItExitRegister(_HTTPAPI_UnbindGroupIDRequestQueue($URLGroupID)) Else _DebugEX_ERROR($FunctionName, "GroupQueueBound:" & $GroupQueueBound) EndIf OnAutoItExitRegister(_HTTPAPI_HttpShutdownRequestQueue($RequestQueue)) Else _DebugEX_ERROR($FunctionName, "RequestQueue:" & $RequestQueue) EndIf Else _DebugEX_ERROR($FunctionName, "URLGroupID:" & $URLGroupID) EndIf OnAutoItExitRegister(_HTTPAPI_HttpCloseServerSession($ServerSessionID)) Else _DebugEX_ERROR($FunctionName, "ServerSessionID:" & $ServerSessionID) EndIf OnAutoItExitRegister(_HTTPAPI_HttpTerminate()) Else _DebugEX_ERROR($FunctionName, "ServerInitialized:" & $ServerInitialized) EndIf Else _DebugEX_ERROR($FunctionName, "EnvironmentInitialized:" & $EnvironmentInitialized) EndIf Return $ReturnValue EndFunc An example use case: #AutoIt3Wrapper_AU3Check_Parameters=-w 3 -w 4 -w 5 -w 6 -d #include <HTTPAPI.au3> _DebugEX_Console("ENABLE") Local $HostURL = "http://127.0.0.1:9000" Local $Path = "/vm" Local $RequestCases = _HTTPAPI_CreateRequestCaseArray() _HTTPAPI_AddRequestCase($RequestCases, "GET", $Path, 200, "OK", "<h1>Hello World</h1>", "text/html") _HTTPAPI_AddRequestCase($RequestCases, "GET", $Path & "/username", 200, "OK", '{"username":"' & @UserName & '"}', "application/json") _HTTPAPI_AddRequestCase($RequestCases, "POST", $Path & "/requestbody", 200, "OK", '$RequestBody', "text/plain", 1) _HTTPAPI_AddRequestCase($RequestCases, "GET", $Path & "/python", 200, "OK", '_HTTPAPI_CaptureCmd("python helloworld.py", "C:\AutoIt\CommunityLibraries\TheXman\HTTPAPI\examples")', "text/plain", 1) _HTTPAPI_Run($HostURL, $Path, $RequestCases) Note: the code has some functions that I use internally for work, you may want to add them in so there aren't any errors: expandcollapse popupGlobal $DEBUG_CONSOLE = 0 Global $DEBUG_COUNTER_ERROR = 0 Global $DEBUG_COUNTER_WARNING = 0 Func _ArraySearchFound($arraysearchresult) Local $FunctionName = "_ArraySearchFound" Local $ReturnValue = 0 If $arraysearchresult <> -1 Then $ReturnValue = 1 Return $ReturnValue EndFunc Func _ArrayEnd(ByRef $thisarray) Local $FunctionName = "_ArrayEnd" Local $ReturnValue = -1 $ReturnValue = UBound($thisarray) - 1 Return $ReturnValue EndFunc Func _StringContainsSubstring($string, $substring) Local $ReturnValue = 0 If StringInStr(String($string), String($substring)) <> 0 Then $ReturnValue = 1 Return $ReturnValue EndFunc Func _DebugEX_ConsoleWrite($thisfunction, $string, $guid = "") Local $ConsoleWriteGUID = "" If $guid <> "" Then $ConsoleWriteGUID = "::::" & $guid Local $ConsoleWriteString = "::::" & $string Local $ConsoleText = @CRLF & $thisfunction & $ConsoleWriteGUID & $ConsoleWriteString If $DEBUG_CONSOLE Then ConsoleWrite($ConsoleText) If _StringContainsSubstring($string, "ERROR ") Then $DEBUG_COUNTER_ERROR += 1 If _StringContainsSubstring($string, "WARNING ") Then $DEBUG_COUNTER_WARNING += 1 EndFunc Func _DebugEX_Console($switch) Switch $switch Case "ENABLE" $DEBUG_CONSOLE = 1 Case "DISABLE" $DEBUG_CONSOLE = 0 EndSwitch EndFunc Func _DebugEX_StartFunc(ByRef $thisfunction) Local $ConsoleText = @CRLF & @CRLF & "starting " & $thisfunction & "..." If $DEBUG_CONSOLE Then ConsoleWrite($ConsoleText) Return $thisfunction EndFunc Func _DebugEX_ERROR($thisfunction, $string, $guid = "") $string = "ERROR " & $string _DebugEX_ConsoleWrite($thisfunction, $string, $guid) EndFunc Func _DebugEX_WARNING($thisfunction, $string, $guid = "") $string = "WARNING " & $string _DebugEX_ConsoleWrite($thisfunction, $string, $guid) EndFunc The code is pretty rough, I haven't polished it up much, but I like the RequestCase array thing Link to comment Share on other sites More sharing options...
argumentum Posted August 15, 2023 Share Posted August 15, 2023 Can this code be forked ?, so that I receive in one script and handle the request in another script ? The reason been that: 1) Say I get a "database query" request and return a value. That is a fast transaction and need not be forked. 2) Say I get a "big file" request and return a value. That is a slow transaction and need to be forked. Follow the link to my code contribution ( and other things too ). FAQ - Please Read Before Posting. Link to comment Share on other sites More sharing options...
TheXman Posted August 15, 2023 Author Share Posted August 15, 2023 5 minutes ago, noellarkin said: I wrote some helper functions for HTTP API as well, perhaps some of these will be useful. Thanks, I will definitely check them out. noellarkin 1 CryptoNG UDF: Cryptography API: Next Gen jq UDF: Powerful and Flexible JSON Processor | jqPlayground: An Interactive JSON Processor Xml2Json UDF: Transform XML to JSON | HttpApi UDF: HTTP Server API | Roku Remote: Example Script About Me How To Ask Good Questions On Technical And Scientific Forums (Detailed) | How to Ask Good Technical Questions (Brief) "Any fool can know. The point is to understand." -Albert Einstein "If you think you're a big fish, it's probably because you only swim in small ponds." ~TheXman Link to comment Share on other sites More sharing options...
TheXman Posted August 15, 2023 Author Share Posted August 15, 2023 3 minutes ago, argumentum said: Can this code be forked ?, so that I receive in one script and handle the request in another script ? If I understand what you are asking for correctly, it seems similar to what @sylremo described HERE. His next post seemed to be a way to handle that type of processing. Personally, I haven't really looked into it because I haven't had a need for it. But at first glance, his proposed solution seemed to be a viable one. argumentum 1 CryptoNG UDF: Cryptography API: Next Gen jq UDF: Powerful and Flexible JSON Processor | jqPlayground: An Interactive JSON Processor Xml2Json UDF: Transform XML to JSON | HttpApi UDF: HTTP Server API | Roku Remote: Example Script About Me How To Ask Good Questions On Technical And Scientific Forums (Detailed) | How to Ask Good Technical Questions (Brief) "Any fool can know. The point is to understand." -Albert Einstein "If you think you're a big fish, it's probably because you only swim in small ponds." ~TheXman Link to comment Share on other sites More sharing options...
KarelM Posted May 17 Share Posted May 17 This is reaaaaly useful! Thank you! I have a question on this: How do I get the IP of the remote request? looks like it is enumerated as "Transport Address Structure", but I am not sure how to get the value of the ptr on the server. It returns a null value. Link to comment Share on other sites More sharing options...
TheXman Posted May 17 Author Share Posted May 17 (edited) Thanks @KarelM Assuming you are using the example script that is provided with the HTTPAPI UDF, you can add the following function to the bottom. The function takes a pointer to the SOCKADDR address structure within the request structure as input, which is in $tRequest.pRemoteAddress, and returns the formatted IPv4 or IPv6 address. expandcollapse popup; #FUNCTION# ==================================================================================================================== ; Name ..........: _SockAddrPtrToIP ; Description ...: Return formatted IPv4 or IPv6 address from pointer to SOCKADDR structure. ; Syntax ........: _HTTPAPI_SocketPtrToIP($pAddress) ; Parameters ....: $pAddress Pointer to SOCKADDR structure. ; Return values .: Success: Formatted IP address string. ; Failure: Empty string and sets the @error flag to non-zero ; @error: 1 - DllStructCreate failed to create SOCKADDR struct ; 2 - Unrecognized address family (AF) value. ; 3 - DllCall to inet_ntop failed ; Author ........: TheXman ; =============================================================================================================================== Func _SockAddrPtrToIP($pAddress) Local $aResult Local $tSOCKADDR Local $tStringBuffer = DllStructCreate("char string[47];") Const $AF_INET = 2 Const $AF_INET6 = 23 Const $tagSOCKADDR = _ "short inFamily;" & _ "byte data[14];" Const $tagSOCKADDR_IN = _ "short inFamily;" & _ "ushort inPort;" & _ "byte inAddr[4];" & _ "byte reserved[8];" Const $tagSOCKADDR_IN6 = _ "short inFamily;" & _ "ushort inPort;" & _ "ulong inFlowInfo;" & _ "byte inAddr[16];" & _ "ulong scopeId;" ; Create struct $tSOCKADDR = DllStructCreate($tagSOCKADDR, $pAddress) If @error Then Return SetError(1, 0, "") ; If address is IPv4 If $tSOCKADDR.inFamily = $AF_INET Then ; Create IPv4 struct $tSOCKADDR = DllStructCreate($tagSOCKADDR_IN, $pAddress) ElseIf $tSOCKADDR.inFamily = $AF_INET6 Then ; Create IPv6 struct $tSOCKADDR = DllStructCreate($tagSOCKADDR_IN6, $pAddress) Else Return SetError(2, 0, "") EndIf ; Get formatted IP address $aResult = _ DllCall("Ws2_32.dll", "str", "inet_ntop", _ "int", $tSOCKADDR.inFamily, _ "ptr", DllStructGetPtr($tSOCKADDR, "inAddr"), _ "struct*", $tStringBuffer, _ "int", DllStructGetSize($tStringBuffer) _ ) If @error Then Return SetError(3, 0, "") Return $aResult[0] EndFunc If you add the following CASE to the process_get_request function in the example script, it will show you how you can invoke the function above: ;/remote addr Case $HTTP_PATH & "/remoteaddr" $iStatusCode = 200 $sReason = "OK" $sAddr = _SockAddrPtrToIP($tRequest.pRemoteAddress) If @error Then ConsoleWrite("Bad return from _SockAddrPtrToIP - @error = " & @error & @CRLF) $sMsg = "<h3>GET request received!</h3>" & _ "Remote address: " & $sAddr & "<br>" When you use "http://127.0.0.1:9000/a3server/remoteaddress", it should give you a response like: Edited May 17 by TheXman CryptoNG UDF: Cryptography API: Next Gen jq UDF: Powerful and Flexible JSON Processor | jqPlayground: An Interactive JSON Processor Xml2Json UDF: Transform XML to JSON | HttpApi UDF: HTTP Server API | Roku Remote: Example Script About Me How To Ask Good Questions On Technical And Scientific Forums (Detailed) | How to Ask Good Technical Questions (Brief) "Any fool can know. The point is to understand." -Albert Einstein "If you think you're a big fish, it's probably because you only swim in small ponds." ~TheXman Link to comment Share on other sites More sharing options...
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now