diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b3ca4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*resharper.user +[Dd]ebug/ +[Rr]elease/ +build/ +[Bb]in/ +[Oo]bj/ +[Hh]elp/ +*.suo +*.sln.cache +_ReSharper.*/ +*.user +packages +doc \ No newline at end of file diff --git a/S3Emulator.sln b/S3Emulator.sln new file mode 100644 index 0000000..c130021 --- /dev/null +++ b/S3Emulator.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Emulator", "src\S3Emulator\S3Emulator.csproj", "{BB9957DE-CD2E-4E76-A1E0-F251A2E91030}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Emulator.Tests", "test\S3Emulator.Tests\S3Emulator.Tests.csproj", "{CFFD14BE-18CE-43D8-8CF6-05DBEBFF03AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S3Emulator.Sample", "src\S3Emulator.Sample\S3Emulator.Sample.csproj", "{F7199CDF-7938-4FA3-8098-B2CEB34AFC81}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030}.Release|Any CPU.Build.0 = Release|Any CPU + {CFFD14BE-18CE-43D8-8CF6-05DBEBFF03AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFFD14BE-18CE-43D8-8CF6-05DBEBFF03AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFFD14BE-18CE-43D8-8CF6-05DBEBFF03AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFFD14BE-18CE-43D8-8CF6-05DBEBFF03AE}.Release|Any CPU.Build.0 = Release|Any CPU + {F7199CDF-7938-4FA3-8098-B2CEB34AFC81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7199CDF-7938-4FA3-8098-B2CEB34AFC81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7199CDF-7938-4FA3-8098-B2CEB34AFC81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7199CDF-7938-4FA3-8098-B2CEB34AFC81}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..6d8d26d --- /dev/null +++ b/build.cmd @@ -0,0 +1,11 @@ +@echo off + +if '%1'=='/?' goto help +if '%1'=='-help' goto help +if '%1'=='-h' goto help + +powershell -NoProfile -ExecutionPolicy Bypass -Command "& '.\psake.ps1' '.\default.ps1' %*; if ($psake.build_success -eq $false) { exit 1 } else { exit 0 }" +goto :eof + +:help +powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0\psake.ps1' -help" diff --git a/default.ps1 b/default.ps1 new file mode 100644 index 0000000..7a03b05 --- /dev/null +++ b/default.ps1 @@ -0,0 +1,20 @@ +properties { + $base_dir = resolve-path . + $tools_dir = "$base_dir\tools" + $sln_file = "$base_dir\S3Emulator.sln" + $version = "1.0.0.0" +} + +task default -depends Compile + +task Nuget { + $nugetConfigs = Get-ChildItem $base_dir -Recurse | ?{$_.name -match "packages\.config"} | select + foreach ($nugetConfig in $nugetConfigs) { + Write-Host "restoring packages from $($nugetConfig.FullName)" + .\Tools\NuGet\NuGet.exe install $($nugetConfig.FullName) /OutputDirectory packages + } +} + +Task Compile -depend Nuget { + msbuild "$sln_file" +} \ No newline at end of file diff --git a/lib/fiddler/FiddlerCore4.XML b/lib/fiddler/FiddlerCore4.XML new file mode 100644 index 0000000..7d0561f --- /dev/null +++ b/lib/fiddler/FiddlerCore4.XML @@ -0,0 +1,5184 @@ + + + + FiddlerCore4 + + + + + Wrapper for WinINET cache APIs. + + + + + Clear all HTTP Cookies from the WinINET Cache + + + + + Clear all files from the WinINET Cache + + + + + Delete all permanent WinINET cookies for sHost; won't clear memory-only session cookies. Supports hostnames with an optional leading wildcard, e.g. *example.com. NOTE: Will not work on VistaIE Protected Mode cookies. + + The hostname whose cookies should be cleared + + + + Clear the Cache items. Note: May be synchronous, may be asynchronous. + + TRUE if cache files should be cleared + TRUE if cookies should be cleared + + + + For PInvoke: Contains information about an entry in the Internet cache + + + + + Wrapper for WinINET proxy configuration APIs + + + + + Hostnames of sites to bypass proxy. <local> is common. + + + + + Allow direct connection to host + + + + + Attempt WPAD autoproxy detection + + + + + Ignore WinINET "no autoproxy unticks box" optimization + + + + + Use user-supplied URL to get FindProxyForURL script + + + + + Use user-supplied manual/fixed proxy address list + + + + + WPAD script url that may be used if _bUseScript true + + + + + true if <local> in sHostsThatBypass + + + + + Gathers proxy information from a named connection. + + Pass DefaultLAN to look for the "null" connection + Proxy info, or null + + + + Get a string describing the proxy settings + + Returns a multi-line string representing the proxy settings + + + + Calculate a string suitable for passing into WinINET APIs. + + Returns a string containing proxy information, or NULL. NB: I've seen WinINET blow up when passed String.Empty rather than null. + + + + + Given a proxy string, we crack out the proxy gateways for each protocol + + e.g. HTTP=itgproxy:80;FTP=ftpprox:21; + false on error + + + + Fills this WinINETProxyInfo instance with settings from specified WinINET connection. + + Name of the connection. Pass NULL for LAN connection. + TRUE if the settings were successfully retrieved. + + + + Sets WinINET proxy settings for specified connection to those specified in this WinINETProxy instance. + + Name of the connection. Pass NULL for LAN connection. + + + + Semi-colon delimited list of hostnames that should bypass the fixed proxy + + + + + TRUE if manually-specified proxy should be used. + + + + + TRUE if a direct HTTP connection may be made if AutoProxy/PAC is unreachable or corrupt + + + + + True if the proxy should be bypassed for dotless hostnames + + + + + String representing the endpoint of the proxy for HTTP-traffic, if configured + + + + + String representing the endpoint of the proxy for HTTPS-traffic, if configured + + + + + String representing the endpoint of the proxy for FTP-traffic, if configured + + + + + String representing the endpoint of the proxy for SOCKS-traffic, if configured + + + + + Bool indicating whether this connection is set to autodetect the proxy + + + + + Returns a string pointing to the ProxyAutoConfig script, or null + + + + + The HTTPSClientHello class is used to parse the bytes of a HTTPS ClientHello message. + + + + + Parse a single extension using the list from http://tools.ietf.org/html/rfc6066 + + + + + + + Parse a single extension using the list from http://tools.ietf.org/html/rfc6066 + + + + + + + The Session object manages the complete HTTP session including the UI listitem, the ServerChatter, and the ClientChatter. + + + + + Sorta hacky, we may use a .NET WebRequest object to generate a valid NTLM response if the server + demands authentication and we don't have a way to get it from the client (e.g. RequestBuilder scenarios) + + + + + Used if the Session is bound to a WebSocket or CONNECTTunnel + + + + + Sets or unsets the specified SessionFlag(s) + + SessionFlags + Desired set value + + + + Test the session's BitFlags + + One or more (OR'd) SessionFlags + TRUE if specified flag(s) are set + + + + Test the session's BitFlags + + One or more (OR'd) SessionFlags + TRUE if any of specified flag(s) are set + + + + Should response be buffered for tampering + + + + + Timers stored as this session progresses + + + + + ListViewItem object associated with this session in the Session list. + + + + + Field is set to False if socket is poisoned due to HTTP errors. + + + + + Object representing the HTTP Response. + + + + + Object representing the HTTP Request. + + + + + Fiddler-internal flags set on the session. + + + + + Contains the bytes of the request body. + + + + + Contains the bytes of the response body. + + + + + IP Address of the client for this session. + + + + + Client port attached to Fiddler. + + + + + IP Address of the server for this session. + + + + + Event object used for pausing and resuming the thread servicing this session + + + + + Returns TRUE if the Session's HTTP Method is available and matches the target method. + + The target HTTP Method being compared. + true, if the method is specified and matches sTestFor (case-insensitive); otherwise false. + + + + Returns TRUE if the Session's target hostname (no port) matches sTestHost (case-insensitively). + + The host to which this session's host should be compared. + True if this session is targete to the specified host. + + + + Replaces any characters in a filename that are unsafe with safe equivalents + + + + + + + Returns HTML representing the session. Call Utilities.StringToCF_HTML on the result of this function before placing it on the clipboard. + + TRUE if only the headers should be copied. + A HTML-formatted fragment representing the current session. + + + + Store this session's request and response to a string. + + If true, return only the request and response headers + String representing this session + + + + Store this session's request and response to a string. + + A string containing the content of the request and response. + + + + This private method pauses the Session's thread to allow breakpoint debugging + + + + + This method resumes the Session's thread in response to "Continue" commands from the UI + + + + + Called by an AcceptConnection-spawned background thread, create a new session object from a client socket and execute the session + + Parameter object defining client socket and endpoint's HTTPS certificate, if present + + + + Call this function while in the "reading response" state to update the responseBodyBytes array with + the partially read response. + + TRUE if the peek succeeded; FALSE if not in the ReadingResponse state + + + + Prevents the server pipe from this session from being pooled for reuse + + + + + Ensures that, after the response is complete, the client socket is closed and not reused + + + + + Immediately close client and server sockets. Call in the event of errors-- doesn't queue server pipes for future reuse. + + + + + + Closes both client and server pipes and moves state to Aborted; unpauses thread if paused. + + + + + Save HTTP response body to Fiddler Captures folder. You likely want to call utilDecodeResponse first. + + True if the response body was successfully saved + + + + Save HTTP response body to specified location. You likely want to call utilDecodeResponse first. + + The name of the file to which the response body should be saved. + True if the file was successfully written. + + + + Save the request body to a file. You likely want to call utilDecodeRequest first. + + The name of the file to which the request body should be saved. + True if the file was successfully written. + + + + Save the request and response to a single file. + + The filename to which the session should be saved. + TRUE if only the headers should be written. + + + + Save the request to a file; the headers' Request Line will not contain the scheme or host + + The name of the file to which the request should be saved. + TRUE to save only the headers + + + + Save the request to a file + + The name of the file to which the request should be saved. + TRUE to save only the headers. + TRUE to include the Scheme and Host in the Request Line. + + + + Read metadata about this session from a stream. Closes stream when done. + + The stream of XML text from which session metadata will be loaded. + True if the Metadata was successfully loaded; False if any exceptions were trapped. + + + + Writes this session's metadata to a file. + + The name of the file to which the metadata should be saved in XML format. + True if the file was successfully written. + + + + Saves the response to a file + + The File to write + TRUE if only heaers should be written + + + + Write the metadata about this session to a stream + + The Stream to write to + + + + Write the session's Request to the specified stream + + TRUE if only the headers should be be written + TRUE if the Scheme and Host should be written in the Request Line + The Stream to which the request should be written + True if the request was written to the stream. False if the request headers do not exist. Throws on other stream errors. + + + + Write the session's Response to the specified stream + + The stream to which the response should be written + TRUE if only the headers should be written + TRUE if the response was written to the stream. False if the response headers do not exist. Throws on other stream errors. + + + + Write the session to the specified stream + + The stream to which the session should be written + TRUE if only the request and response headers should be written + False on any exceptions; True otherwise + + + + Replace HTTP request headers and body using the specified file. + + The file containing the request + True if the file was successfully loaded as the request body + + + + Replace HTTP response headers and body using the specified file. + + The file containing the response. + True if the file was successfully loaded. + + + + Return a string generated from the request body, decoding it and converting from a codepage if needed. Throws on errors. + + A string containing the request body. + + + + Return a string generated from the response body, decoding it and converting from a codepage if needed. Throws on errors. + + A string containing the response body. + + + + Find the text encoding of the request + + Returns the Encoding of the requestBodyBytes + + + + Find the text encoding of the response + + The Encoding of the responseBodyBytes + + + + Returns true if the absolute request URI contains the specified string. Case-insensitive. + + Case-insensitive string to find + TRUE if the URI contains the string + + + + Removes chunking and HTTP Compression from the Response. Adds or updates Content-Length header. + + Returns TRUE if the response was decoded; returns FALSE on failure, or if response didn't have headers that showed encoding. + + + + Removes chunking and HTTP Compression from the Request. Adds or updates Content-Length header. + + Returns TRUE if the request was decoded; returns FALSE on failure, or if request didn't have headers that showed encoding. + + + + Use GZIP to compress the response body. Throws exceptions to caller. + + TRUE if compression succeeded + + + + Use DEFLATE to compress the response body. Throws exceptions to caller. + + TRUE if compression succeeded + + + + Use BZIP2 to compress the response body. Throws exceptions to caller. + + TRUE if compression succeeded + + + + Introduces HTTP Chunked encoding on the response body + + The number of chunks to try to create + TRUE if the chunking could be performed. + + + + Perform a string replacement on the request body. Adjusts the Content-Length header if needed. + + The case-sensitive string to search for. + The text to replace. + TRUE if one or more replacements occurred. + + + + Call inside OnBeforeRequest to create a response object and bypass the server. + + + + + Perform a regex-based string replacement on the response body. Adjust the Content-Length header if needed. + + The regular expression used to search the body + The text or expression used to replace + TRUE if replacements occured + + + + Perform a string replacement on the response body (potentially multiple times). Adjust the Content-Length header if needed. + + String to find (case-sensitive) + String to use to replace + TRUE if replacements occurred + + + + Perform a one-time string replacement on the response body. Adjust the Content-Length header if needed. + + String to find (case-sensitive) + String to use to replace + TRUE for Case-Sensitive + TRUE if a replacement occurred + + + + Replaces the request body with sString as UTF8. Sets Content-Length header and removes Transfer-Encoding/Content-Encoding + + The desired request Body as a string + + + + Replaces the response body with sString. Sets Content-Length header and removes Transfer-Encoding/Content-Encoding + + The desired response Body as a string + + + + Add a string to the top of the response body, updating Content-Length. (Call utilDecodeResponse first!) + + The string to prepend + + + + Find a string in the request body. Return its index, or -1. + + Term to search for + Require case-sensitive match? + Location of sSearchFor,or -1 + + + + Find a string in the response body. Return its index, or -1. + + Term to search for + Require case-sensitive match? + Location of sSearchFor,or -1 + + + + Reset the SessionID counter to 0. This method can lead to confusing UI, so use sparingly. + + + + + Create a session object from two byte[] representing request and response. + + The client data bytes + The server data bytes + + + + Create a session object from two byte[] representing request and response. This is used when loading a Session Archive Zip. + + The client data bytes + The server data bytes + SessionFlags for this session + + + + Creates a new session and attaches it to the pipes passed as arguments + + The client pipe from which the request is read and to which the response is written. + The server pipe to which the request is sent and from which the response is read. May be null. + + + + Initialize a new session from a given request headers and bodyrequest builder data. + + NB: If you're copying an existing request, use oRequestHeaders.Clone() + The bytes of the request's body + + + + Called when the session is ready to begin processing. Eats exceptions to prevent unhandled exceptions on background threads from killing the application. + + Dummy parameter + + + + Fiddler cannot auto-follow a redirect to a protocol other than HTTP/HTTPS/FTP. + TODO: Should pass the combined URL into this function + + The BASE URL to which a relative redirection should be applied + Response "Location" header + TRUE if the auto-redirect target is allowed + + + + InnerExecute() implements Fiddler's HTTP Pipeline + + + + + Assign a Session ID. Called by ClientChatter when headers are available + + + + + Called only by InnerExecute, this method reads a request from the client and performs tampering/manipulation on it. + + + + + + Returns TRUE if response is a challenge + + True for HTTP/401,407 with NEGO or NTLM demand + + + + Replace the "ipv*.fiddler "fake" hostnames with the IP-literal equvalents. + + + + + Determines if request host is pointing directly at Fiddler. + + + + + + Echo the client's request back as a HTTP Response, encoding to prevent XSS. + + + + + Echo the client's request back as a HTTP Response, encoding to prevent XSS. + + + + + Send the Fiddler Root certificate back to the client + + + + + This method indicates to the client that a secure tunnel was created, + without actually talking to an upstream server. + + If Fiddler's AutoResponder is enabled, and that autoresponder denies passthrough, + then Fiddler itself will always indicate "200 Connection Established" and wait for + another request from the client. That subsequent request can then potentially be + handled by the AutoResponder engine. + + + The hostname to use in the Certificate returned to the client + + + + This method adds a Proxy-Support: Session-Based-Authentication header and indicates whether the response is Nego:Type2. + + Returns TRUE if server returned a credible Type2 NTLM Message + + + + This helper evaluates the conditions for client socket reuse. + + + + + + Sends the Response Fiddler received from the server back to the client socket. + + True, if the response was successfully sent to the client + + + + Creates a new session, binding this session's pipes to that new session, as appropriate. When this method is called, the + nextSession variable is populated with the new session + + TRUE if both the client and server pipes should be bound regardless of the serverPipe's ReusePolicy + + + + Refresh the UI elements for the session. + + Use TRUE to call Invoke, use FALSE to call BeginInvoke. + + + + Bitflags of commonly-queried session attributes + + + + + If this session is a Tunnel, and the tunnel's IsOpen property is TRUE, returns TRUE. Otherwise returns FALSE. + + + + + If this session is a Tunnel, returns number of bytes sent from the Server to the Client + + + + + If this session is a Tunnel, returns number of bytes sent from the Client to the Server + + + + + Returns True if this is a HTTP CONNECT tunnel. + + + + + This event fires at any time the session's State changes. Use with caution due to the potential for performance impact. + + + + + Gets or Sets the HTTP Request body bytes. + Setter adjusts Content-Length header, and removes Transfer-Encoding and Content-Encoding headers. + Setter will throw if the Request object does not exist for some reason. + Use utilSetRequestBody(sStr) to ensure proper character encoding if you need to use a string. + + + + + Gets or Sets the HTTP Response body bytes. + Setter adjusts Content-Length header, and removes Transfer-Encoding and Content-Encoding headers. + Setter will throw if the Response object has not yet been created. (See utilCreateResponseAndBypassServer) + Use utilSetResponseBody(sStr) to ensure proper character encoding if you need to use a string. + + + + + When true, this session was conducted using the HTTPS protocol. + + + + + When true, this session was conducted using the FTP protocol. + + + + + Get the process ID of the application which made this request, or 0 if it cannot be determined. + + + + + Gets a path-less filename suitable for the Response entity. Uses Content-Disposition if available. + + + + + Set to true in OnBeforeRequest if this request should bypass the gateway + + + + + Returns the port used by the client to communicate to Fiddler. + + + + + State of session. Note Side-Effects: If setting to .Aborted, calls FinishUISession. If setting to/from a Tamper state, calls RefreshMyInspectors + + + + + Returns the path and query part of the URL. (For a CONNECT request, returns the host:port to be connected.) + + + + + Retrieves the complete URI, including protocol/scheme, in the form http://www.host.com/filepath?query. + Or sets the complete URI, adjusting the UriScheme and/or Host. + + + + + Gets or sets the URL (without protocol) being requested from the server, in the form www.host.com/filepath?query. + + + + + DNS Name of the host server targeted by this request. NB: a port# may be included. + + + + + DNS Name of the host server (no port) targeted by this request. Will include IPv6-literal brackets for IPv6-literal addresses + + + + + Returns the server port to which this request is targeted. + + + + + Returns the sequential number of this session. Note, by default numbering is restarted at zero when the session list is cleared. + + + + + Returns the Address used by the client to communicate to Fiddler. + + + + + Gets or Sets the HTTP Status code of the server's response + + + + + Returns TRUE if this session's State > ReadingResponse, and oResponse, oResponse.headers, and responseBodyBytes are all non-null + + + + + Indexer property into SESSION flags, REQUEST headers, and RESPONSE headers. e.g. oSession["Request", "Host"] returns string value for the Request host header. If null, returns String.Empty + + SESSION, REQUEST or RESPONSE + The name of the flag or header + String value or String.Empty + + + + Simple indexer into the Session's oFlags object + + + + + Event arguments constructed for the OnStateChanged event raised when a Session's state property changed + + + + + The prior state of this session + + + + + The new state of this session + + + + + Constructor for the change in state + + The old state + The new state + + + + This class holds a specialized memory stream with growth characteristics more suitable for reading from a HTTP Stream. + The default MemoryStream's Capacity will always grow to 256 bytes, then at least ~2x current capacity up to 1gb, then to the exact length after that. + This has two problems: + + The capacity may unnecessarily grow to >85kb, putting the object on the LargeObjectHeap even if we didn't really need 85kb. + After the capacity reaches 1gb in length, the capacity growth never exceeds the length, leading to huge reallocations and copies on every write. + + In some cases, the client can "hint" what the proper capacity ultimately needs to be by adding the Header size in bytes to the Content-Length specified size. + + + + + Used by the caller to supply a hint on the expected total size of reads from the pipe. + We cannot blindly trust this value because sometimes the client or server will lie and provide a + huge value that it will never use. This is common for RPC-over-HTTPS tunnels like that used by Outlook, for instance. + + Suggested total buffer size in bytes + + + + Interface for the WebSocket and CONNECT Tunnel classes + + + + + The CONNECTTunnel class represents a "blind tunnel" through which a CONNECT request is serviced to shuffle bytes between a client and the server. + + + See pg 206 in HTTP: The Complete Reference for details on how Tunnels work. + When HTTPS Decryption is disabled, Fiddler accepts a CONNECT request from the client. Then, we open a connection to the remote server. + We shuttle bytes back and forth between the client and the server in this tunnel, keeping Fiddler itself out of the loop + (no tampering, etc). + + + + + Number of bytes received from the client + + + + + Number of bytes received from the server + + + + + TRUE if this is a Blind tunnel, FALSE if decrypting + + + + + This "Factory" method creates a new HTTPS Tunnel and executes it on a background (non-pooled) thread. + + The Session containing the HTTP CONNECT request + + + + Creates a HTTPS tunnel. External callers instead use the CreateTunnel static method. + + The session for which this tunnel was initially created. + Client Pipe + Server Pipe + + + + This function keeps the thread alive until it is signaled that the traffic is complete + + + + + Executes the HTTPS tunnel on a background thread + + + + + Close the HTTPS tunnel and signal the event to let the service thread die. + WARNING: This MUST not be allowed to throw any exceptions, because it will do so on threads that don't catch them, and this will kill the application. + + + + + Called when we have received data from the local client. + Incoming data will immediately be forwarded to the remote host. + + The result of the asynchronous operation. + + + Called when we have sent data to the local client.
When all the data has been sent, we will start receiving again from the remote host.
+ The result of the asynchronous operation. +
+ + Called when we have sent data to the remote host.
When all the data has been sent, we will start receiving again from the local client.
+ The result of the asynchronous operation. +
+ + Called when we have received data from the remote host.
Incoming data will immediately be forwarded to the local client.
+ The result of the asynchronous operation. +
+ + + Returns number of bytes sent from the Server to the Client + + + + + Returns number of bytes sent from the Client to the Server + + + + + Holds a variety of useful functions used in Fiddler and its addons. + + + + + Queries the user for a filename + + Dialog title + String representing file dialog filter + Filename or null + + + + Queries the user for a filename + + Dialog title + String representing file dialog filter + Initial directory or null + Filename or null + + + + Queries the user for an OPEN filename + + Dialog title + String representing file dialog filter (e.g. "All files (*.*)|*.*") + Filename or null + + + + Queries the user for an OPEN filename + + Dialog title + String representing file dialog filter + Initial directory or null + Filename or null + + + + Check to see that the target assembly defines a RequiredVersionAttribute and that the current Fiddler instance meets that requirement + + The assembly to test + The "type" of extension for display in error message + TRUE if the assembly includes a requirement and Fiddler meets it. + + + + Typically, a version number is displayed as "major number.minor number.build number.private part number". + + Version required + Version of the binary being tested + Returns 0 if exact match, else greater than 0 if Required version greater than verTest + + + + TODO: Stop using this. The static Control.ModifierKeys property is the proper way to check for a key's pressed state. + + + + + + + Convert a full path into one that uses environment variables + + e.g. C:\windows\system32\foo.dll + %WINDIR%\System32\foo.dll + + + + Ensure that the target file does not yet exist. If it does, generates a new filename with an embedded identifier, e.g. out[1].txt instead + + Candidate filename + New filename which does not yet exist + + + + Ensure that the target path exists and if a file exists there, it is not readonly or hidden + + The candidate filename + + + + Writes arrBytes to a file, creating the target directory and overwriting if the file exists. + + Path to File to write. + Bytes to write. + + + + Fills an array completely using the provided stream. Unlike a normal .Read(), this one will always fully fill the array unless the Stream throws. + + The stream from which to read. + The byte array into which the data should be stored. + The count of bytes read. + + + + Returns the Value from a (case-insensitive) token in the header string. Correctly handles double-quoted strings. Allows comma and semicolon as delimiter + + Name of the header + Name of the token + Value of the token if present; otherwise, null + + + + Ensures that the target string is iMaxLength or fewer characters + + The string to trim from + The maximum number of characters to return + Up to iMaxLength characters from the "Head" of the string. + + + + Returns the "Head" of a string, before and not including a specified search string. + + The string to trim from + The delimiting string at which the trim should end. + Part of a string up to (but not including) sDelim, or the full string if sDelim was not found. + + + + Returns the "Head" of a string, before and not including the first instance of specified delimiter. + + The string to trim from. + The delimiting character at which the trim should end. + Part of a string up to (but not including) chDelim, or the full string if chDelim was not found. + + + + [Deprecated] Ensures that the target string is iMaxLength or fewer characters + + The string to trim from + The maximum number of characters to return + Identical to the method. + Up to iMaxLength characters from the "Head" of the string. + + + + Returns the "Tail" of a string, after (but NOT including) the First instance of specified delimiter. + + + The string to trim from. + The delimiting character after which the text should be returned. + Part of a string after (but not including) chDelim, or the full string if chDelim was not found. + + + + Returns the "Tail" of a string, after (but NOT including) the First instance of specified search string. + + + The string to trim from. + The delimiting string after which the text should be returned. + Part of a string after (but not including) sDelim, or the full string if sDelim was not found. + + + + Returns the "Tail" of a string, after (and including) the first instance of specified search string. + + The string to trim from. + The delimiting string at which the text should be returned. + Part of the string starting with sDelim, or the entire string if sDelim not found. + + + + Returns the "Tail" of a string, after (but not including) the Last instance of specified delimiter. + + + The string to trim from. + The delimiting character after which text should be returned. + Part of a string after (but not including) the final chDelim, or the full string if chDelim was not found. + + + + Returns the "Tail" of a string, after (but not including) the Last instance of specified substring. + + + The string to trim from. + The delimiting string after which text should be returned. + Part of a string after (but not including) the final sDelim, or the full string if sDelim was not found. + + + + Determines true if a request with the specified HTTP Method/Verb MUST contain a entity body + + The Method/Verb + TRUE if the HTTP Method MUST contain a request body. + + + + Determines true if a request with the specified HTTP Method/Verb may contain a entity body + + The Method/Verb + TRUE if the HTTP Method MAY contain a request body. + + + + Determines if the specified MIME type is "binary" in nature. + + The MIME type + TRUE if the MIME type is likely binary in nature + + + + Gets a string from a byte-array, stripping a Byte Order Marker preamble if present. + + + This function really shouldn't need to exist. Why doesn't calling .GetString on a string with a preamble remove the preamble??? + + The byte array + The encoding to convert from + The string + + + + Gets (via Headers or Sniff) the provided body's text Encoding. If not found, returns CONFIG.oHeaderEncoding (usually UTF-8). Potentially slow. + + HTTP Headers, ideally containing a Content-Type header with a charset attribute. + byte[] containing the entity body. + A character encoding, if one could be determined + + + + Gets (via Headers or Sniff) the Response Text Encoding. Returns CONFIG.oHeaderEncoding (usually UTF-8) if unknown. + Perf: May be quite slow; cache the response + + The session + The encoding of the response body + + + + Set of encodings for which we'll attempt to sniff. List ordered from longest BOM to shortest + + + + + HtmlEncode a string. + In Fiddler itself, this is a simple wrapper for the System.Web.HtmlEncode function. + The .NET3.5/4.0 Client Profile doesn't include System.Web, so we must provide our + own implementation of HtmlEncode for FiddlerCore's use. + + String to encode + String encoded according to the rules of HTML Encoding, or null. + + + + Replaces System.Web.HttpUtility.UrlPathEncode(str). + + String to encode as a URL Path + Encoded string + + + + Tokenize a string into tokens. Delimits on unquoted whitespace ; quote marks are dropped unless preceded by \ characters. + BUG BUG: Doesn't do what you expect with a path like this, due to the trailing slash: + prefs set fiddler.config.path.webtestexport.plugins "F:\users\ericlaw\documents\fiddler2\importexport\VSWebTest\" + For now, the simple bet is to drop the final backslash (since it'll get put back by other code) + + The string to tokenize + An array of strings + + + + Pretty-print a Hex view of a byte array. Slow. + + The byte array + Number of bytes per line + String containing a pretty-printed array + + + + Pretty-print a Hex view of a byte array. Slow. + + The byte array + Number of bytes per line + The maximum number of bytes to pretty-print + String containing a pretty-printed array + + + + Print an byte array to a hex string. + Slow. + + Byte array + String of hex bytes + + + + Create a string in CF_HTML format + + The HTML string + The HTML string wrapped with a CF_HTML prelude + + + + Returns an integer from the registry, or a default. + + The Registry key in which to find the value. + The registry value name. + Default to return if the registry key is missing or cannot be used as an integer + The retrieved integer, or the default. + + + + Save a string to the registry. Correctly handles null Value, saving as String.Empty + + The registry key into which the value will be written. + The name of the value. + The value to write. + + + + Returns an Float from the registry, or a default. + + Registry key in which to find the value. + The value name. + The default float value if the registry key is missing or cannot be used as a float. + Float representing the value, or the default. + + + + Get a bool from the registry + + The RegistryKey + The Value name + The default value + Returns an bool from the registry, or bDefault if the registry key is missing or cannot be used as an bool. + + + + Maps a MIMEType to a file extension. Note: May hit the registry, so consider the performance implications. Pass oResponse.MIMEType as input, to ensure no charset info in the data. + + The MIME Type + A file extension for the type, or .TXT + + + + Use the system registry to find the proper MIME-Type for a given file extension + + File extension (e.g. ".js") + Content-Type, or null if one cannot be determined + + + + Determines if we have a complete chunked response body (RFC2616 Section 3.6.1) + + The session object, used for error reporting + The response data stream. Note: We do not touch the POSITION property. + The start of the HTTP body to scan for chunk size info + Returns the start of the final received/partial chunk + End of byte data in stream representing this chunked content, or -1 if error + True, if we've found the complete last chunk, false otherwise. + + + + Takes a byte array and applies HTTP Chunked Transfer Encoding to it + + The byte array to convert + The number of chunks to try to create + The byte array with Chunked Transfer Encoding applied + + + + Removes HTTP chunked encoding from the data in writeData and returns the resulting array. + + Some chunked data + Unchunked data. Warning: Throws on data format errors + + + + Remove all encodings from arrBody, based on those specified in the supplied HTTP headers. Throws on errors. + + Readonly Headers specifying what encodings are applied + In/Out array to be modified + + + + GZIPs a byte-array + + Input byte array + byte[] containing a gzip-compressed copy of writeData[] + + + + GZIP-Expand function which shows no UI and will throw on error + + TRUE if you want to use Xceed to decompress; false if you want to use System.IO + byte[] to decompress + A decompressed byte array, or byte[0]. Throws on errors. + + + + Expands a GZIP-compressed byte array + + The array to decompress + byte[] containing an un-gzipped copy of compressedData[] + + + + Compress a byte array using RFC1951 DEFLATE + + Array to compress + byte[] containing a DEFLATE'd copy of writeData[] + + + + UnDeflate function which shows no UI and will throw on error + + TRUE if you want to use Xceed to decompress; false if you want to use System.IO + byte[] to decompress + A decompressed byte array, or byte[0]. Throws on errors. + + + + Decompress a byte array that was compressed using RFC1951 DEFLATE + + Array to decompress + byte[] of decompressed data + + + + Compress a byte[] using the bzip2 algorithm + + Array to compress + byte[] of data compressed using bzip2 + + + + Decompress an array compressed using bzip2 + + The array to expand + byte[] of decompressed data + + + + Try parsing the string for a Hex-formatted int. If it fails, return false and 0 in iOutput. + + The hex number + The int value + TRUE if the parsing succeeded + + + + Returns TRUE if two ORIGIN (scheme+host+port) values are functionally equivalent. + + The first ORIGIN + The second ORIGIN + The default port, if a port is not specified + TRUE if the two origins are equivalent + + + + This function cracks a sHostPort string to determine if the address + refers to a "local" site + + The string to evaluate, potentially containing a port + True if the address is local + + + + This function cracks a sHostPort string to determine if the address + refers to the local computer + + The string to evaluate, potentially containing a port + True if the address is 127.0.0.1, 'localhost', or ::1 + + + + Determines if the specified Hostname is a either 'localhost' or an IPv4 or IPv6 loopback literal + + Hostname (no port) + TRUE if the hostname is equivalent to localhost + + + + This function cracks the Hostname/Port combo, removing IPV6 brackets if needed + + Hostname/port combo, like www.foo.com or www.example.com:8888 or [::1]:80 + The hostname, minus any IPv6 literal brackets, if present + Port #, 80 if not specified, 0 if corrupt + + + + Given a string/list in the form HOSTNAME:PORT#;HOSTNAME2:PORT2#, this function returns the FIRST IPEndPoint. Defaults to port 80 if not specified. + Warning: DNS resolution is slow, so use this function wisely. + + HOSTNAME:PORT#;OPTHOST2:PORT2# + An IPEndPoint or null + + + + Given a string/list in the form HOSTNAME:PORT#;HOSTNAME2:PORT2#, this function returns all IPEndPoints for ALL listed hosts. Defaults to port 80 if not specified. + Warning: DNS resolution is slow, so use this function wisely. + + HOSTNAME:PORT#;OPTHOST2:PORT2# + An array of IPEndPoints or null if no results were obtained + + + + This function attempts to be a ~fast~ way to return an IP from a hoststring that contains an IP-Literal. + + Hostname + IPAddress, or null, if the sHost wasn't an IP-Literal + + + + Launch the user's browser to a hyperlink. This function traps exceptions and notifies the user via UI dialog. + + The URL to ShellExecute. + TRUE if the ShellExecute call succeeded. + + + + Wrapper for Process.Start that shows error messages in the event of failure. + + Fully-qualified filename to execute. + Command line parameters to pass. + TRUE if the execution succeeded. FALSE if the error message was shown. + + + + Run an executable and wait for it to exit, notifying the user of any exceptions. + + Fully-qualified filename of file to execute. + Command-line parameters to pass. + TRUE if the execution succeeded. FALSE if the error message was shown. + + + + Run an executable, wait for it to exit, and return its output as a string. + NOTE: Uses CreateProcess, so you cannot launch applications which require Elevation. + + Fully-qualified filename of file to Execute + Command-line parameters to pass + Exit code returned by the executable + String containing the standard-output of the executable + + + + Copy a string to the clipboard, notifying the user of any exceptions + + The text to copy + TRUE if the copy succeeded + + + + Copy an object to the clipboard, notifying the user of any exceptions + + The object to copy + True if successful + + + + This method prepares a string to be converted into a regular expression by escaping special characters. + This method was originally meant for parsing WPAD proxy script strings, but is now used in other places. You should probably be using the Static RegEx.Escape method for most purposes instead. + + + + + + + + + Determines whether the arrData array begins with the arrMagics bytes. Used for Content-Type sniffing. + + The data, or null + The MagicBytes to look for + TRUE if arrData begins with arrMagics + + + + Determines whether the arrData array begins with the sMagics ASCII bytes. Used for Content-Type sniffing. + + The data, or null + The ASCII text to look for + TRUE if arrData begins with sMagics (encoded as ASCII octets) + + + + Determine if a given byte array has the start of a HTTP/1.* 200 response. + Useful primarily to determine if a CONNECT request to a proxy returned success. + + + + + + + For a given process name, returns a bool indicating whether this is a known browser process name. + + The Process name (e.g. "abrowser.exe") + Returns true if the process name starts with a common browser process name (e.g. ie, firefox, etc) + + + + Ensure that a given path is absolute, if not, applying the root path + + + + + + + + If sFilename is absolute, returns it, otherwise, combines the leaf filename with local response folders hunting for a match. + Trims at the first ? character, if any + + Either a fully-qualified path, or a leaf filename + File path + + + + Format an Exception message, including InnerException information if present. + + + + + + + URLMon Interop Class + + + + + Set the user-agent string for the current process + + New UA string + + + + Query WinINET for the current process' proxy settings. Oddly, there's no way to UrlMkGetSessionOption for the current proxy. + + String of hex suitable for display + + + + Configures the current process to use the system proxy for URLMon/WinINET traffic. + + + + + Configures the current process to use no Proxy for URLMon/WinINET traffic. + + + + + Sets the proxy for the current process to the specified list. See http://msdn.microsoft.com/en-us/library/aa383996(VS.85).aspx + + e.g. "127.0.0.1:8888" or "http=insecProxy:80;https=secProxy:444" + Semi-colon delimted list of hosts to bypass proxy; use <local> to bypass for Intranet + + + + State of the current session + + + + + Object created but nothing's happening yet + + + + + Thread is reading the HTTP Request + + + + + AutoTamperRequest pass 1 (Only used by IAutoTamper) + + + + + User can tamper using Fiddler Inspectors + + + + + AutoTamperRequest pass 2 (Only used by IAutoTamper) + + + + + Thread is sending the Request to the server + + + + + Thread is reading the HTTP Response + + + + + AutoTamperResponse pass 1 (Only used by IAutoTamper) + + + + + User can tamper using Fiddler Inspectors + + + + + AutoTamperResponse pass 2 (Only used by IAutoTamper) + + + + + Sending response to client application + + + + + Session complete + + + + + Session was aborted (client didn't want response, fatal error, etc) + + + + + This enumeration provides the values for the Session object's BitFlags field + + + + + No flags are set + + + + + The request originally arrived with a URL specifying the HTTPS protocol. + + + + + The request originally arrived with a URL specifying the FTP protocol. + + + + + RESERVED FOR FUTURE USE. Do not use. + + + + + The client pipe was reused + + + + + The server pipe was reused + + + + + RESERVED FOR FUTURE USE. Do not use. + + + + + The response was streamed + + + + + The request was generated by Fiddler itself (e.g. the Composer tab) + + + + + The response was generated by Fiddler itself (e.g. AutoResponder or utilCreateResponseAndBypassServer) + + + + + This session was loaded from a .SAZ File + + + + + This session was loaded from some other tool + + + + + This request was sent to an upstream (CERN) gateway proxy + + + + + This is a "blind" CONNECT tunnel for HTTPS traffic + + + + + This is a CONNECT tunnel which decrypts HTTPS traffic as it flows through + + + + + This response was served from a client cache, bypassing Fiddler. Fiddler only "sees" this session because other software reported it to Fiddler + + + + + There was a HTTP Protocol violation in the client's request + + + + + There was a HTTP Protocol violation in the server's response + + + + + Response body was dropped, e.g due to fiddler.network.streaming.ForgetStreamedData + + + + + This is a CONNECT tunnel for WebSocket traffic + + + + + This request was sent using the SOCKS protocol + + + + + This class maintains the Proxy Bypass List for the upstream gateway. + In the constructor, pass the desired proxy bypass string retrieved from WinINET. + Then, call the IsBypass(sTarget) method to determine if the Gateway should be bypassed + + + + + Pass the desired proxy bypass string retrieved from WinINET. + + + + + + Given the rules for this bypasslist, should this target bypass the proxy? + + + + + + + Convert the string representing the bypass list into an array of rules escaped and ready to be turned into regular expressions + + + + + + This function converts the internal bypassList into a list of regular expressions + + + + + Flags that indicate what problems, if any, were encountered in parsing HTTP headers + + + + + There were no problems parsing the HTTP headers + + + + + The HTTP headers ended incorrectly with \n\n + + + + + The HTTP headers ended incorrectly with \n\r\n + + + + + The HTTP headers were malformed. + + + + + The Parser class exposes static methods used to parse strings or byte arrays into HTTP messages. + + + + + Given a byte[] representing a request, determines the offsets of the components of the line. WARNING: Input MUST contain a LF or an exception will be thrown + + Byte array of the request + Returns the index of the byte of the URI in the Request line + Returns the length of the URI in the Request line + Returns the index of the first byte of the name/value header pairs + + + + Parse out HTTP Header lines. + + Header collection to update + Array of Strings + Index into array at which parsing should start + String containing any errors encountered + TRUE if there were no errors, false otherwise + + + + Given a byte array, determines the Headers length + + Input array of data + Returns the calculated length of the headers. + Returns the calculated start of the response body. + Any HTTPHeaderParseWarnings discovered during parsing. + True, if the parsing was successful. + + + + Parse the HTTP Request into a headers object. + + The HTTP Request string, including at least the headers terminated by CRLFCRLF + HTTPRequestHeaders parsed from the string. + + + + Parse the HTTP Response into a headers object. + + The HTTP response as a string, including at least the headers. + HTTPResponseHeaders parsed from the string. + + + + The RASInfo class is used to enumerate Network Connectoids so Fiddler can adjust proxy configuration for all connectoids, not just the DefaultLAN + + + + + Ask RAS for the list of network connectoids. We'll always add "DefaultLAN" to this list as well. + + + + + + Abstract base class for the ClientPipe and ServerPipe classes. A Pipe represents a connection to either the client or the server, optionally encrypted using SSL/TLS. + + + + + List of sessions for which this pipe was used. This field will be removed in a future release. + + + + + The base socket wrapped in this pipe + + + + + The number of times that this Pipe has been used + + + + + The HTTPS stream wrapped around the base socket + + + + + The display name of this Pipe + + + + + Number of milliseconds to delay each 1024 bytes transmitted + + + + + Create a new pipe, an enhanced wrapper around a socket + + Socket which this pipe wraps + Identification string used for debugging purposes + + + + Call this method when about to reuse a socket. Currently, increments the socket's UseCount and resets its transmit delay to 0. + + The session identifier of the new session, or zero + + + + Sends a byte array through this pipe + + The bytes + + + + Sends the data specified in oBytes (between iOffset and iOffset+iCount-1 inclusive) down the pipe. + + + + + + + + Receive bytes from the pipe into the DATA buffer. + + Throws IO exceptions from the socket/stream + Array of data read + Bytes read + + + + Return the raw socket this pipe wraps. Avoid calling this method if at all possible. + + The Socket object this Pipe wraps. + + + + Shutdown and close the socket inside this pipe. Eats exceptions. + + + + + Abruptly closes the socket by sending a RST packet + + + + + Return the Connected status of the base socket + + + + + Returns a bool indicating if this Pipe is used to carry WebSocket traffic + + + + + Returns a bool indicating if the socket in this Pipe is CURRENTLY connected and wrapped in a SecureStream + + + + + Return the Remote Port to which this socket is attached. + + + + + Return the Local Port to which the base socket is attached. Note: May return a misleading port if the ISA Firewall Client is in use. + + + + + Returns the remote address to which this Pipe is connected + + + + + Gets or sets the transmission delay on this Pipe, used for performance simulation purposes. + + + + + Summary description for frmPrompt. + + + + + Required designer variable. + + + + + GetUserString prompts the user for a string. + + Title of the dialog + The prompt text in the dialog + The default response + If true, will return null if user hits cancel. Else returns sDefault. + The user's result, or null if user cancelled and bReturnNullIfCancelled set. + + + + Clean up any resources being used. + + + + + Required method for Designer support - do not modify + the contents of this method with the code editor. + + + + + To override default certificate handling, your class should implement this interface in an assembly + referenced by the fiddler.certmaker.assembly preference; by default, "certmaker.dll" in the application + folder is loaded + + + + + Implement ICertificateProvider2 instead + + + + + Return a certificate to secure this traffic. Generally, it's expected that this method WILL create a new certificate if needed. + + Hostname (e.g. "www.example.com") + An X509Certificate, or null on error + + + + Return the root certificate to which Host Certificates are chained. Generally, it's expected that this method will NOT create a root certificate. + + An X509Certificate, or null on error + + + + When this method is called, your extension should create a Root certificate. + + TRUE if the operation was successful + + + + When this method is called, your extension should copy the your Root certificate into + the user's (or machines's) Root certificate store. + + TRUE if the operation was successful + + + + When this method is called, your extension should discard all certificates and + clear any certificates that have been added to the user's certificate store. + + TRUE, if all certificates were removed; FALSE if any certificates were preserved + + + + When this method is called, your extension should check to see if the User or Machine Root + certificate store contains your Root certificate. + + Set to TRUE if StoreLocation.CurrentUser StoreName.Root has the certificate + Set to TRUE if StoreLocation.LocalMachine StoreName.Root has the certificate + TRUE if either bUserTrusted or bMachineTrusted + + + + When this method is called, your extension should discard all certificates and + clear any certificates that have been added to the user's certificate store + + TRUE if the root certificate should also be cleared + TRUE, if all certificates were removed; FALSE if any certificates were preserved + + + + File path pointing to the location of MakeCert.exe + + + + + Cache of previously-generated EE certificates. Thread safety managed by _oRWLock + + + + + Cache of previously-generated Root certificate + + + + + Reader/Writer lock gates access to the certificate cache and generation functions. + + We must set the SupportsRecursion flag because there are cases where the thread holds the lock in Write mode and then enters Read mode in a nested call. + + + + Constructor: Simply initializes the path to MakeCert + + + + + Find certificates that have the specified full subject. + + The store to search + FindBySubject{Distinguished}Name requires a complete match of the SUBJECT, including CN, O, and OU + Matching certificates + + + + Find certificates that have the specified issuer. + + The store to search + FindByIssuer{Distinguished}Name requires a complete match of the SUBJECT, including CN, O, and OU + Matching certificates + + + + Interface method: Clear the in-memory caches and Windows certificate stores + + TRUE to clear the Root Certificate from the cache and Windows stores + TRUE if successful + + + + Interface method: Clear the in-memory caches and Windows certificate stores + + + + + + Use MakeCert to generate a unique self-signed certificate + + TRUE if the Root certificate was generated successfully + + + + Get the root certificate, only if it already exists. + + + + + + Returns an Interception certificate for the specified hostname + + Hostname for the target certificate + This method uses a Reader lock when checking the cache and a Writer lock when updating the cache. + An Interception Certificate, or NULL + + + + Find a certificate from the certificate store, creating a new certificate if it was not found. + + A SubjectCN hostname, of the form www.example.com + TRUE if the cert wasn't found in the Windows Certificate store and this function attempted to create it. + No locks are acquired by this method itself. + A certificate or /null/ + + + + Find a certificate from the certificate store, optionally Creating a new certificate if it was not found. + + No locks are acquired by this method itself. + A certificate or /null/ + + + + Creates a certificate for ServerAuth. If isRoot is set, designates that this is a self-signed root. + + Uses a reader lock when checking for the Root certificate. Uses a Writer lock when creating a certificate. + A string of the form: "www.hostname.com" + A boolean indicating if this is a request to create the root certificate + Newly-created certificate, or Null + + + + The ClientChatter object, exposed as the oRequest object on the Session object, represents a single web request. + + + + + Size of buffer passed to pipe.Receive when reading from the client + + + + + The ClientPipe object which is connected to the client, or null. + + + + + Parsed Headers + + + + + The session object which owns this ClientChatter + + + + + The host pulled from the URI, usually null except while reading/parsing the request + + + + + Buffer holds this request's data as it is read from the pipe. + + + + + Offset to first byte of body in m_requestData + + + + + Optimization: tracks how far we've previously looked when determining iEntityBodyOffset + + + + + Create a ClientChatter object initialized with a set of HTTP headers + Called primarily when loading session data from a file. + + The session object which will own this request + The string containing the request data + + + + Loads a HTTP request from a file rather than a memory stream. TODO: Why not make this public and have a responsebody version? + + The file to load + TRUE if the file existed. + + + + Based on this session's data, determine the expected Transfer-Size of the request body. See RFC2616 Section 4.4 Message Length. + Note, there's currently no support for "multipart/byteranges" requests anywhere in Fiddler. + + Expected Transfer-Size of the body, in bytes. + + + + Free Request data. Called by TakeEntity or by ReadRequest method on request failure + + + + + Extract a byte array representing the entity, put any excess bytes back in the socket, delete the requestData stream, and return the entity. + + Byte array containing the entity + + + + Send a HTTP/XXX Error Message to the Client, calling FiddlerApplication.BeforeReturningError and DoReturningError in FiddlerScript. + Note: This method does not poison either the client or server pipe, so if poisoning is desired, it's the caller's responsibility to do that. + + Response code + Response status text + Body of the HTTP Response + + + + Parse the headers from the requestData buffer. + Precondition: Call AFTER having set the correct iEntityBodyOffset. + + Note: This code used to be a lot simpler before, when it used strings instead of byte[]s. Sadly, + we've gotta use byte[]s to ensure nothing in the URI gets lost. + + TRUE if successful. Frees arrRequest if successful. + + + + This function decides if the request string represents a complete HTTP request + + + + + + Scans requestData stream for the \r\n\r\n (or variants) sequence + which indicates that the header block is complete. + + SIDE EFFECTS: + iBodySeekProgress is updated and maintained across calls to this function + iEntityBodyOffset is updated if the end of headers is found + + True, if requestData contains a full set of headers + + + + Read a complete HTTP Request from the Client. + + TRUE, if a request could be read. FALSE, otherwise. + + + + HTTP Headers sent in the client request, or null. + + + + + Was this request received from a reused client connection? + + + + + Note: This returns the HTTP_HOST header, which may include a trailing port #. + + + + + Simple indexer into the Request Headers object + + + + + The PipePool maintains a collection of connected ServerPipes for reuse + + + + + The Pool itself + + + + + Returns the number of Pipes in the pool + + The number of pipes, or 0 if the pool is empty + + + + Remove any pipes from Queues if they exceed the age threshold + Remove any Queues from pool if they are empty + + + + + Dump all of the queues from the hashtable + + + + + Return a string representing the Pipes in the Pool + + A string representing the pipes in the pool + + + + Dequeue a server connection for reuse. + + The key which identifies the connection to search for. Good syntax is [HTTPS:]HOSTNAME:PORT + The ProcessID of the client requesting a Pipe + HACK to be removed; the SessionID# of the request + + + + + Queue a connection for later use. + + The Pipe to place in the queue + + + + A ClientPipe wraps a socket connection to a client application. + + + + + If you previously read more bytes than you needed from this client socket, you can put some back. + + Array of bytes to put back + + + + Sets the receiveTimeout based on whether this is a freshly opened client socket or a reused one. + + + + + Returns a semicolon-delimited string describing this ClientPipe + + A semicolon-delimited string + + + + Perform a HTTPS Server handshake to the client. Swallows exception and returns false on failure. + + + + + + + This function sends the client socket a CONNECT ESTABLISHED, and then performs a HTTPS authentication + handshake, with Fiddler acting as the server. + + Hostname Fiddler is pretending to be (NO PORT!) + The set of headers to be returned to the client in response to the client's CONNECT tunneling request + true if the handshake succeeds + + + + ID of the process that opened this socket, assuming that Port Mapping is enabled, and the connection is from the local machine + + + + + Name of the Process referred to by LocalProcessID, or String.Empty if unknown + + + + + The Logger object is a simple event log + + + + + Queue of Messages that are be logged (usually during application startup) until another object has loaded and registered for notification of such Messages + + + + + Creates a Logger object + + True if a queue should be created to store messages during Fiddler's startup + + + + Flushes previously-queued messages to the newly attached listener. + + + + + Log a string with specified string formatting + + The format string + The arguments to replace in the string + + + + Log a string + + The string to log + + + + The Event to raise when a string is logged + + + + + EventArgs class for the LogEvent handler + + + + + The String which has been logged + + + + + Fiddler Transcoders allow import and export of Sessions from Fiddler + + + + + Create the FiddlerTranscoders object + + + + + Add Import/Export encoders to FiddlerApplication.oTranscoders + + Assembly to import exporters and importers + FALSE on obvious errors + + + + Add Import/Export encoders to FiddlerApplication.oTranscoders + + Assembly to scan for transcoders + FALSE on obvious errors + + + + Loads any assembly in the specified path that ends with .dll and does not start with "_", checks that a compatible version requirement was specified, + and adds the importer and exporters within to the collection. + + The path to scan for extensions + + + + Ensures that Import/Export Transcoders have been loaded + + + + + Returns a TranscoderTuple willing to handle the specified format + + The Format + TranscoderTuple, or null + + + + Returns a TranscoderTuple willing to handle the specified format + + The Format + TranscoderTuple, or null + + + + Gets the format list of the specified type and adds that type to the collection. + + + + TRUE if any formats were found; FALSE otherwise + + + + Clear Importer and Exporter collections + + + + + True if one or more classes implementing ISessionImporter are available. + + + + + True if one or more classes implementing ISessionImporter are available. + + + + + The WebSocket class represents a "tunnel" through a WebSocket shuffles bytes between a client and the server. + + + + + Number of bytes received from the client + + + + + Number of bytes received from the server + + + + + This factory method creates a new WebSocket Tunnel and executes it on a background (non-pooled) thread. + + The Session containing the HTTP CONNECT request + + + + Creates a WebSocket tunnel. External callers instead use the CreateTunnel static method. + + The session for which this tunnel was initially created. + The client pipe + The server pipe + + + + This function keeps the thread alive until it is signaled that the traffic is complete + + + + + Executes the WebSocket tunnel on a background thread + + + + + Close the tunnel and signal the event to let the service thread die. Called by oSession.Abort() + WARNING: This should not be allowed to throw any exceptions, because it will do so on threads that don't catch them, and this will kill the application. + + + + + Called when we have received data from the local client. + Incoming data will immediately be forwarded to the remote host. + + The result of the asynchronous operation. + + + Called when we have sent data to the local client.When all the data has been sent, we will start receiving again from the remote host. + The result of the asynchronous operation. + + + Called when we have sent data to the remote host.When all the data has been sent, we will start receiving again from the local client. + The result of the asynchronous operation. + + + Called when we have received data from the remote host. Incoming data will immediately be forwarded to the local client. + The result of the asynchronous operation. + + + + Boolean that determines whether the WebSocket tunnel tracks messages. + + + + + Returns number of bytes sent from the Server to the Client + + + + + Returns number of bytes sent from the Client to the Server + + + + + The MockTunnel represents a CONNECT tunnel which was reloaded from a SAZ file. + + + + + The CONFIG object is Fiddler's legacy settings object, introduced before the advent of the Preferences system. + + + + + True if this is a "Viewer" instance of Fiddler that will not persist its settings + + + + + Boolean controls whether Fiddler should map inbound connections to their original process using IPHLPAPI + + + + + Boolean controls whether Fiddler should attempt to decrypt HTTPS Traffic + + + + + Boolean controls whether Fiddler will attempt to use the Server Name Indicator TLS extension to generate the SubjectCN for certificates + + + + + Returns 127.0.0.1:{ListenPort} or fiddler.network.proxy.RegistrationHostName:{ListenPort} + + + + + Use 128bit AES Encryption when password-protecting .SAZ files. Note that, while this + encryption is much stronger than the default encryption algorithm, it is significantly + slower to save and load these files, and the Windows Explorer ZIP utility cannot open them. + + + + + SSL/TLS Protocols we allow the client to choose from (when we call AuthenticateAsServer) + + + + + SSL/TLS Protocols we request the server use (when we call AuthenticateAsClient) + + + + + Version information for the Fiddler/FiddlerCore assembly + + + + + Controls whether Fiddler will send traffic to the previously-set system proxy + + + + + The encoding with which HTTP Headers should be parsed. Defaults to UTF8, but may be overridden by specifying a REG_SZ containing the encoding name in the registry key \Fiddler2\HeaderEncoding + + + + + Controls whether Fiddler will reuse server connections for multiple sessions + + + + + Controls whether Fiddler will reuse client connections for multiple sessions + + + + + Controls whether Fiddler should register as the HTTPS proxy + + + + + Controls whether Fiddler should register as the FTP proxy + + + + + Controls whether Fiddler will try to write exceptions to the System Event log. Note: Usually fails due to ACLs on the Event Log. + + + + + Controls whether Fiddler will attempt to log on to the upstream proxy server to download the proxy configuration script + + + + + Controls whether Fiddler will attempt to connect to IPv6 addresses + + + + + Name of connection to which Fiddler should autoattach if MonitorAllConnections is not set + + + + + The username to send to the upstream gateway if the Version Checking webservice request requires authentication + + + + + The password to send to the upstream gateway if the Version Checking webservice request requires authentication + + + + + Set this flag if this is a "temporary" port (E.g. specified on command line) and it shouldn't be overridden in the registry + + + + + Controls whether Certificate-Generation output will be spewed to the Fiddler Log + + + + + List of hostnames for which HTTPS decryption (if enabled) should be skipped + + + + + Check to see if WinINET settings are configured for one-proxy-per-machine. If so, the WinINET API to set + the proxy will fail unless Fiddler has write permissions to HKLM (e.g. running elevated). + + + + + Test to see if ISA Firewall client is in annoying mode + + + + + Return a Special URL. + + String constant describing the URL to return. CASE-SENSITIVE! + Returns target URL + + + + Get a registry path for a named constant + + The path to retrieve [Root, UI, Dynamic, Prefs] + The registry path + + + + Return an app path, ending in "\" or a filename + + CASE-SENSITIVE + The specified filesystem path + + + + Ensure that the per-user folders used by Fiddler are present. + + + + + Control which processes have HTTPS traffic decryption enabled + + + + + Port to which Fiddler should forward inbound requests when configured to run as a Reverse Proxy + + + + + On attach, will configure WinINET to bypass Fiddler for these hosts. + + + + + Boolean indicating whether Fiddler will open the listening port exclusively + + + + + Controls whether server certificate errors are ignored when decrypting HTTPS traffic. + + + + + Controls whether notification dialogs and prompts should be shown. + + + + + The port upon which Fiddler is configured to listen. + + + + + Returns the path and filename of the editor used to edit the Rules Javascript file. + + + + + Returns true if Fiddler should permit remote connections. Requires restart. + + + + + A simple Process Type enumeration used by various filtering features + + + + + Include all Processes + + + + + Processes which appear to be Web Browsers + + + + + Processes which appear to NOT be Web Browsers + + + + + Include only traffic where Process ID isn't known (e.g. remote clients) + + + + + EventArgs for preference-change events. See http://msdn.microsoft.com/en-us/library/ms229011.aspx. + + + + + The name of the preference being added, changed, or removed + + + + + The string value of the preference, or null if the preference is being removed + + + + + Returns TRUE if ValueString=="true", case-insensitively + + + + + The IFiddlerPreferences Interface is exposed by the FiddlerApplication.Prefs object, and enables + callers to Add, Update, and Remove preferences, as well as observe changes to the preferences. + + + + + Store a boolean value for a preference + + The named preference + The boolean value to store + + + + Store an Int32 value for a preference + + The named preference + The int32 value to store + + + + Store a string value for a preference + + The named preference + The string value to store + + + + Get a preference's value as a boolean + + The Preference Name + The default value for missing or invalid preferences + A Boolean + + + + Gets a preference's value as a string + + The Preference Name + The default value for missing preferences + A string + + + + Gets a preference's value as a 32-bit integer + + The Preference Name + The default value for missing or invalid preferences + An integer + + + + Removes a named preference from storage + + The name of the preference to remove + + + + Add a Watcher that will be notified when a value has changed within the specified prefix. + + The prefix of preferences for which changes are interesting + The Event handler to notify + Returns the Watcher object added to the notification list + + + + Removes a previously-created preference Watcher from the notification queue + + The Watcher to remove + + + + Indexer. Returns the value of the preference as a string + + The Preference Name + The Preference value as a string, or null + + + + The PreferenceBag is used to maintain a threadsafe Key/Value list of preferences, persisted in the registry, and with appropriate eventing when a value changes. + + + + + Load the existing preferences from the registry into the Preferences bag. + + + + + Serialize the existing preferences to the Registry. + + + + + Get a string array of the preference names + + string[] of preference names + + + + Gets a preference's value as a string + + The Preference Name + The default value if the preference is missing + A string + + + + Return a bool preference. + + The Preference name + The default value to return if the specified preference does not exist + The boolean value of the Preference, or the default value + + + + Return an Int32 Preference. + + The Preference name + The default value to return if the specified preference does not exist + The Int32 value of the Preference, or the default value + + + + Update or create a string preference. + + The name of the Preference + The value to assign to the Preference + + + + Update or create a Int32 Preference + + The name of the Preference + The value to assign to the Preference + + + + Update or create a Boolean preference. + + The name of the Preference + The value to assign to the Preference + + + + Delete a Preference from the collection. + + The name of the Preference to be removed. + + + + Remove all Watchers + + + + + Remove all watchers and write the registry. + + + + + Return a description of the contents of the preference bag + + Multi-line string + + + + Return a string-based serialization of the Preferences settings. + + TRUE for a multi-line format with all preferences + String + + + + Add a watcher for changes to the specified preference or preference branch. + + Preference branch to monitor, or String.Empty to watch all + The EventHandler accepting PrefChangeEventArgs to notify + Returns the PrefWatcher object which has been added, store to pass to RemoveWatcher later. + + + + Remove a previously attached Watcher + + The previously-specified Watcher + + + + This function executes on a single background thread and notifies any registered + Watchers of changes in preferences they care about. + + A string containing the name of the Branch that changed + + + + Spawn a background thread to notify any interested Watchers of changes to the Target preference branch. + + The arguments to pass to the interested Watchers + + + + Returns a string naming the current profile + + + + + Indexer into the Preference collection. + + The name of the Preference to update/create or return. + The string value of the preference, or null. + + + + A simple struct which contains a Branch identifier and EventHandler + + + + + The HostList allows fast determination of whether a given host is in the list. It supports leading wildcards (e.g. *.foo.com), and the special tokens <local> and <loopback>. + Note: List is *not* threadsafe; instead of updating it, construct a new one. + + + + + Generate an empty HostList + + + + + Create a hostlist and assign it an initial set of sites + + List of hostnames, including leading wildcards, and optional port specifier. Special tokens are *, <local>, <nonlocal>, and <loopback>. + + + + Clear the HostList + + + + + Clear the Hostname and assign the new string as the contents of the list. + + List of hostnames, including leading wildcards, and optional port specifier. Special tokens are *, <local>, <nonlocal>, and <loopback>. + TRUE if the list was constructed without errors + + + + Clear the Hostname and assign the new string as the contents of the list. + + List of hostnames, including leading wildcards, and optional port specifier. Special tokens are *, <local>, <nonlocal>, and <loopback>. + Outparam string containing list of parsing errors + TRUE if the list was constructed without errors + + + + Return the current list of rules as a string + + String containing current rules, using "; " as a delimiter between entries + + + + Determine if a given Host is in the list + + A Host string, potentially including a port + TRUE if the Host's hostname matches a rule in the list + + + + Determine if a given Hostname is in the list + + A hostname, NOT including a port + TRUE if the hostname matches a rule in the list + + + + Determine if a given Host:Port pair matches an entry in the list + + A hostname, NOT including the port + The port + TRUE if the hostname matches a rule in the list + + + + This private tuple allows us to associate a Hostname and a Port + + + + + Port specified in the rule + + + + + Hostname specified in the rule + + + + + Create a new HostPortTuple + + + + + The policy which describes how this pipe may be reused by a later request. Ordered by least restrictive to most. + + + + + The ServerPipe may be freely reused by any subsequent request + + + + + The ServerPipe may be reused only by a subsequent request from the same client process + + + + + The ServerPipe may be reused only by a subsequent request from the same client pipe + + + + + The ServerPipe may not be reused for a subsequent request + + + + + A ServerPipe wraps a socket connection to a server. + + + + + User-controlled list of Certificate Serial #s for which Fiddler should not raise a warning about certificate errors + + + + + DateTime of the completion of the TCP/IP Connection + + + + + TickCount when this Pipe was last placed in a PipePool + + + + + Returns TRUE if this ServerPipe is connected to a Gateway + + + + + Returns TRUE if this ServerPipe is connected to a SOCKS gateway + + + + + The Pooling key used for reusing a previously pooled ServerPipe + + + + + This field, if set, tracks the process ID to which this Pipe is permanently bound; set by MarkAsAuthenticated + + + + + Backing field for the isAuthenticated property + + + + + String containing representation of the server's certificate + + + + + Wraps a socket in a Pipe + + The Socket + Pipe's human-readable name + True if the Pipe is attached to a gateway + The Pooling key used for socket reuse + + + + Marks this socket as having been authenticated. Depending on the preference "fiddler.network.auth.reusemode" this may impact the reuse policy for this pipe + + The client's process ID, if known. + + + + Returns a semicolon-delimited string describing this ServerPipe + + A semicolon-delimited string + + + + Return a string describing the HTTPS connection security, if this socket is secured + + A string describing the HTTPS connection's security. + + + + Get the Transport Context for the underlying HTTPS connection so that Channel-Binding Tokens work correctly + + + + + + Get the user's default client cert for authentication; caching if if possible and permitted. + + + + + + This method is called by the HTTPS Connection establishment to optionally attach a client certificate to the request. + Test Page: https://tower.dartmouth.edu/doip/OracleDatabases.jspx + + + + + + + + + + + Return a Certificate Collection containing the specified certificate. Fixup path if needed. + + + + + + + This function secures an existing connection and authenticates as client. This is primarily useful when + the socket is connected to a Gateway/Proxy and we had to send a CONNECT and get a HTTP/200 Connected back before + we actually secure the socket. + http://msdn.microsoft.com/en-us/library/system.net.security.sslstream.aspx + + The CN to use in the certificate + Path to client certificate file + Key to use in the HTTPS Server connection pool + Reference-passed integer which returns the time spent securing the connection + TRUE if the connection can be secued + + + + Policy for reuse of this pipe + + + + + Returns TRUE if there is an underlying, mutually-authenticated HTTPS stream. + + + + + Returns TRUE if this PIPE is marked as having been authenticated using a Connection-Oriented Auth protocol: NTLM, Kerberos, or HTTPS Client Certificate + + + + + Indicates if this pipe is connected to an upstream Proxy. + + + + + Indicates if this pipe is connected to a SOCKS gateway + + + + + Gets the pooling key for this server pipe + + + + + Returns the IPEndPoint to which this socket is connected + + + + + CodeDescription attributes are used to enable the FiddlerScript Editor to describe available methods, properties, fields, and events. + + + + + CodeDescription attributes should be constructed by annotating a property, method, or field. + + The descriptive string which should be displayed for this this property, method, or field + + + + The descriptive string which should be displayed for this this property, method, or field + + + + + Flags that can be passed into the Startup method + + + + + No options. + + + + + Register with WinINET as the System Proxy + + + + + Decrypt HTTPS Traffic + + + + + Accept requests from remote computers or devices + + + + + Set this flag to forward requests to any upstream gateway + + + + + Set this flag to set all WinINET connections to use Fiddler, otherwise only the Local LAN is pointed to Fiddler + + + + + Start FiddlerCore with the default set of options (RegisterAsSystemProxy | DecryptSSL | AllowRemoteClients | ChainToUpstreamGateway | MonitorAllConnections | CaptureLocalhostTraffic) + + + + + A simple event handling delegate for functions which accept no parameters. + + + + + An event handling delegate which is called during report calculation with the set of sessions being evaluated. + + The sessions in this report. + + + + An event handling delegate which is called as a part of the HTTP pipeline at various stages. + + The Web Session in the pipeline. + + + + This class acts as the central point for script/extensions to interact with Fiddler components. + + + + + TRUE if Fiddler is currently shutting down + + + + + The default certificate used for client authentication + + + + + Fiddler's "Janitor" clears up unneeded resources (e.g. server sockets, DNS entries) + + + + + Gets Fiddler* version info + + A string indicating the build/flavor of the Fiddler* assembly + + + + Returns Help/About information. + + Display string describing the current Fiddler instance. + + + + Fiddler's core proxy object. + + + + + Fiddler Import/Export Transcoders + + + + + Checks if FiddlerCore is running. + + TRUE if FiddlerCore is started/listening; FALSE otherwise. + + + + Checks if FiddlerCore is running and registered as the System Proxy. + + TRUE if FiddlerCore IsStarted AND registered as the system proxy; FALSE otherwise. + + + + Recommended way to Start FiddlerCore listening on the specified port + + The port + The FiddlerCoreStartupFlags option you want to set; FiddlerCoreStartupFlags.Default is recommended + + + + Start the FiddlerCore engine; this overload is NOT RECOMMENDED + + + + + + + + Start the FiddlerCore engine; this overload is NOT RECOMMENDED + + Port to Listen on. + Boolean indicating if FiddlerCore should register as the system proxy. + Boolean indicating if FiddlerCore should decrypt secure traffic. If true, requires MakeCert.exe in the Application folder. + Boolean indicating if FiddlerCore should accept connections from remote computers. Note: You must ensure Firewall is configured to allow such connections to your program. + + + + Start a new proxy endpoint instance, listening on the specified port + + The port to listen on + TRUE if remote clients should be permitted to connect to this endpoint + A Hostname (e.g. EXAMPLE.com) if this endpoint should be treated as a HTTPS Server + A Proxy object, or null if unsuccessful + + + + Start a new proxy endpoint instance, listening on the specified port + + The port to listen on + TRUE if remote clients should be permitted to connect to this endpoint + A certificate to return when clients connect, or null + A Proxy object, or null if unsuccessful + + + + Shuts down the FiddlerCore proxy and disposes it. Note: If there's any traffic in progress while you're calling this method, + your background threads are likely to blow up with ObjectDisposedExceptions or NullReferenceExceptions. In many cases, you're + better off simply calling oProxy.Detach() and letting the garbage collector clean up when your program exits. + + + + + Notify a listener that a block of a response was read. + + The session for which the response is being read + byte buffer (not completely full) + bytes set. + FALSE if AbortReading was set + + + + Invoke the BeforeInspect event + + The session being Inspected + Return TRUE if the default inspection behavior should occur + + + + Export Sessions in the specified format + + Shortname of desired format + Sessions to export + Options to pass to the ISessionExport interface + Your callback event handler, or NULL to allow Fiddler to handle + TRUE if successful, FALSE if desired format doesn't exist or other error occurs + + + + Calls a Fiddler Session Importer and returns the list of loaded Sessions. + + String naming the Import format, e.g. HTTPArchive + Should sessions be added to WebSessions list? (Not meaningful for FiddlerCore) + Dictionary of Options to pass to the Transcoder + Your callback event handler, or NULL to allow Fiddler to handle + Loaded Session[], or null on Failure + + + + Reset the SessionID counter to 0. This method can lead to confusing UI, so call sparingly. + + + + + Report an exception to the user. + + The Exception + The Title of the dialog + + + + Report an exception to the user. + + The Exception + The Title of the dialog + + + + Show the user a message when an HTTP Error was encountered + + Session with error + Set to true to prevent pooling/reuse of client connection + The SessionFlag which should be set to log this violation + Set to true to prevent pooling/reuse of server connection + Information about the problem + + + + We really don't want this method to get inlined, because that would cause the Xceed DLLs to get loaded in the Main() function instead + of when _SetXceedLicenseKeys is called; that, in turn, would delay the SplashScreen. + + + + + Used to track errors with addons. + + + + + + + Fiddler's logging system + + + + + Fiddler's Preferences collection. Learn more at http://fiddler.wikidot.com/prefs + + + + + This event fires when the user instructs Fiddler to clear the cache or cookies + + + + + This event fires each time FiddlerCore reads data from network for the server's response. Note that this data + is not formatted in any way, and must be parsed by the recipient. + + + + + This event fires before a session is Inspected (e.g. double-clicked in the Web Sessions list). Set the CANCEL flag to prevent Fiddler's default behavior. + + + + + This event fires when a client request is received by Fiddler + + + + + This event fires when a server response is received by Fiddler + + + + + This event fires when Request Headers are available + + + + + This event fires when Response Headers are available + + + + + This event fires when an error response is generated by Fiddler + + + + + This event fires when a session has been completed + + + + + This event fires when a user notification would be shown. See CONFIG.QuietMode property. + + + + + This event fires when Fiddler evaluates the validity of a server-provided certificate. Adjust the value of the ValidityState property if desired. + + + + + Sync this event to be notified when FiddlerCore has attached as the system proxy.")] + + + + + Sync this event to be notified when FiddlerCore has detached as the system proxy. + + + + + EventArgs class for the OnNotification handler + + + + + The string message of the notification + + + + + Enumeration of possible responses specified by the ValidateServerCertificateEventArgs as modified by FiddlerApplication's OnValidateServerCertificate event + + + + + The certificate will be considered valid if CertificatePolicyErrors == SslPolicyErrors.None, otherwise the certificate will be invalid unless the user manually allows the certificate. + + + + + The certificate will be confirmed with the user even if CertificatePolicyErrors == SslPolicyErrors.None. + Note: FiddlerCore does not support user-prompting and will always treat this status as ForceInvalid. + + + + + Force the certificate to be considered Valid, regardless of the value of CertificatePolicyErrors. + + + + + Force the certificate to be considered Invalid, regardless of the value of CertificatePolicyErrors. + + + + + These EventArgs are passed to the FiddlerApplication.OnValidateServerCertificate event handler when a server-provided HTTPS certificate is evaluated + + + + + EventArgs for the ValidateServerCertificateEvent that allows host to override default certificate handling policy + + The session + The CN expected for this session + The certificate provided by the server + The certificate chain of that certificate + Errors from default validation + + + + The port to which this request was targeted + + + + + The SubjectCN (e.g. Hostname) that should be expected on this HTTPS connection, based on the request's Host property. + + + + + The Session for which a HTTPS certificate was received. + + + + + The server's certificate chain. + + + + + The SslPolicyErrors found during default certificate evaluation. + + + + + Set this property to override the certificate validity + + + + + The X509Certificate provided by the server to vouch for its authenticity + + + + + These EventArgs are constructed when FiddlerApplication.OnClearCache is called. + + + + + Constructs the Event Args + + Should Cache Files be cleared? + Should Cookies be cleared? + + + + True if the user wants cache files to be cleared + + + + + True if the user wants cookies to be cleared + + + + + When the FiddlerApplication.OnReadResponseBuffer event fires, the raw bytes are available via this object. + + + + + Set to TRUE to request that Import/Export process be aborted as soon as convenient + + + + + Session for which this responseRead is occurring + + + + + Byte buffer returned from read. Note: Always of fixed size, check iCountOfBytes to see which bytes were set + + + + + Count of latest read from Socket. If less than 1, response was ended. + + + + + Clear the DNS Cache. Called by the NetworkChange event handler in the oProxy object + + + + + Remove all expired DNSCache entries; called by the Janitor + + + + + Show the contents of the DNS Resolver cache + + + + + + Gets first available IP Address from DNS. Throws if address not found! + + String containing the host + True to use Fiddler's DNS cache. + IPAddress of target, if found. + + + + Gets IP Addresses for host from DNS. Throws if address not found! + + String containing the host + True to use Fiddler's DNS cache. + The Timers object to which the DNS lookup time should be stored, or null + List of IPAddresses of target, if any found. + + + + Trim an address list, removing duplicate entries, IPv6-entries if-disabled, and entries beyond the fifth one + + The list to filter + A filtered address list + + + + A DNSCacheEntry holds a cached resolution from the DNS + + + + + TickCount of this record's creation + + + + + IPAddresses for this hostname + + + + + Construct a new cache entry + + The address information to add to the cache + + + + This class is used to find and create certificates in the Windows Certificate store, in order + to use the certificates for use in Fiddler's HTTPS man-in-the-middle capability. + + + + + Enables specification of a delegate certificate provider that generates certificates for HTTPS interception. + + + + + Load a delegate Certificate Provider + + The provider, or null + + + + Removes Fiddler-generated certificates from the Windows certificate store + + + + + Removes Fiddler-generated certificates from the Windows certificate store + + Indicates whether Root certificates should also be cleaned up + + + + Returns the Root certificate that Fiddler uses to generate per-site certificates used for HTTPS interception. + + Returns the root certificate, if present, or null if the root certificate does not exist. + + + + Find a certificate from the certificate store. + + A string of the form: "www.hostname.com" + A certificate or /null/ + + + + Determine if the self-signed root certificate exists + + True if the Root certificate returned from GetRootCertificate is non-null, False otherwise. + + + + Is Fiddler's root certificate in the Root store? + + TRUE if so + + + + Is Fiddler's root certificate in the Machine Root store? + + TRUE if so + + + + Create a self-signed certificate to use for HTTPS interception + + TRUE if successful + + + + Finds the Fiddler root certificate and prompts the user to add it to the TRUSTED store + + True if successful + + + + This class allows fast-lookup of a ProcessName from a ProcessID. + + + + + Static constructur which registers for cleanup + + + + + Prune the cache of expiring PIDs + + + + + Map a Process ID (PID) to a Process Name + + The PID + A Process Name (e.g. IEXPLORE.EXE) or String.Empty + + + + Structure mapping a Process ID (PID) to a ProcessName + + + + + The TickCount when this entry was created + + + + + The ProcessName (e.g. IEXPLORE.EXE) + + + + + Create a PID->ProcessName mapping + + The ProcessName (e.g. IEXPLORE.EXE) + + + + The WinHTTPAutoProxy class is used to handle upstream gateways when the client was configured to use WPAD or an Proxy AutoConfig (PAC) script. + + + + + Indication as to whether AutoProxy information is valid. 0=Unknown/Enabled; 1=Valid/Enabled; -1=Invalid/Disabled + + + + + Returns a string containing the currently selected autoproxy options + + + + + + Returns a single-line string containing the currently selected autoproxy options + + + + + + Get WPAD-discovered URL for display purposes; note that we don't actually use this when determining the gateway, + instead relying on the WinHTTPGetProxyForUrl function to do this work for us. + + A WPAD url, if found, or String.Empty + + + + Return gateway endpoint for requested Url. TODO: Add caching layer! TODO: Support multiple results? + + The URL for which the gateway should be determined + The Endpoint of the Gateway, or null + TRUE if WinHttpGetProxyForUrl succeeded + + + + Close the WinHTTP Session handle + + + + + Note: Be sure to use the same hSession to prevent redownload of the proxy script + + + + + Set to true to send Negotiate creds when challenged to download the script + + + + + TODO: Need to refactor visibility here. Right now, _oConnectoids is (internal) instead of (private) because the + Options dialog iterates the list. And about:connectoids wants access too + + + + + Return the configured default connectiod name, if specified + + Either DefaultLAN or the user-specified connectoid name + + + + Map a local port number to the originating process ID + + The local port number + The originating process ID + + + + Calls the GetExtendedTcpTable function to map a port to a process ID. + This function is (over) optimized for performance. + + Client port + AF_INET or AF_INET6 + PID, if found, or 0 + + + + Given a local port number, uses GetExtendedTcpTable to find the originating process ID. + First checks the IPv4 connections, then looks at IPv6 connections + + Client applications' port + ProcessID, or 0 if not found + + + + Enumeration of possible queries that can be issued using GetExtendedTcpTable + http://msdn2.microsoft.com/en-us/library/aa366386.aspx + + + + + The ServerChatter object is responsible for transmitting the Request to the destination server and retrieving its Response. + + + + + Size of buffer passed to pipe.Receive when reading from the server + + + + + The pipeServer represents Fiddler's connection to the server. + + + + + The session to which this ServerChatter belongs + + + + + The inbound headers on this response + + + + + Indicates whether this request was sent to the Gateway. + Session should have SessionFlags.SentToGateway set. + + + + + Buffer holds this response's data as it is read from the pipe. + + + + + The total count of bytes read for this response. Typically equals m_responseData.Length unless log-drop-response-body flag is set and + Streaming mode is enabled, in which case it will be larger since the m_responseData is cleared after every read. + + + + + Pointer to first byte of Entity body (or to the start of the next set of headers in the case where there's a HTTP/1xx intermediate header) + Note: This gets reset to 0 if we're streaming and dropping the response body. + + + + + Optimization: tracks how far we've looked into the Request when determining iEntityBodyOffset + + + + + True if HTTP Response headers have been returned to the client. + + + + + Indicates how much of _responseData buffer has already been streamed to the client + + + + + Position in responseData of the start of the latest parsed chunk size information + + + + + Peek at the current response body and return it as an array + + The response body as an array, or byte[0] + + + + Create a new ServerChatter object. + + + + + + Create a ServerChatter object and initialize its headers from the specified string + + + + + + + Clear the current object and start over + + If TRUE, allocates a buffer (m_responseData) to read from a pipe. If FALSE, nulls m_responseData. + + + + Scans responseData stream for the \r\n\r\n (or variants) sequence + which indicates that the header block is complete. + + SIDE EFFECTS: + iBodySeekProgress is updated and maintained across calls to this function + iEntityBodyOffset is updated if the end of headers is found + + True, if responseData contains a full set of headers + + + + Parse the HTTP Response into Headers and Body. + + + + + + Attempts to get Headers from the stream. If a HTTP/100 Continue block is present, it is removed and ignored. + + + + + Deletes a single header block (at this point, always a HTTP/1xx header block) from the Response stream + and adjusts all header-reading state to start over from the top of the stream. If the fiddler.network.streaming.leakhttp1xx preference is TRUE, + then the 1xx message will have been leaked before calling this method. + + + + + Adjusts PipeServer's ReusePolicy if response headers require closure. Then calls _detachServerPipe() + + + + + Queues or End()s the ServerPipe, depending on its ReusePolicy + + + + + Determines whether a given PIPE is suitable for this session based on this session's target, PID, etc. + + The Client Process ID, if any + + + TRUE if the connection should be used, FALSE otherwise + + + + Connect to the Server or Gateway + Note that HTTPS Tunnels use a different code path. + + Note that this function is crazy complicated due to the intricacies of socket reuse. We want to avoid + redundant DNS lookups etc, and we need to be sensitive to the fact that the Gateway can change from request to request. + + TODO: Move this into the Pipe code and into the new PipePool. probably should be a static that returns a pipe? + + TRUE, if pipeServer was assigned a connection to the target server + + + + Sends a CONNECT to the specified socket acting as a gateway proxy and waits for a 200 OK response. This method is used when Fiddler needs + a HTTPS Connection through a proxy but the client didn't establish one (e.g. the Request Composer). Note: May THROW + + The Socket + The host to which the gateway should connect + The port to which the gateway should connect + The User-Agent String + TRUE if a 200 OK was received, FALSE if something else was received, THROWS EXCEPTION on network errors + + + + Given an address list and port, attempts to create a socket to the first responding host in the list (retrying via DNS Failover if needed). + + IPEndpoints to attempt to reach + Session object to annotate with timings and errors + Connected Socket. Throws Exceptions on errors. + + + + Sends the HTTP Request to the upstream server or proxy + + True if connection and send succeeded, False otherwise + + + + Loads a HTTP response from a file + + The name of the file from which a response should be loaded + False if the file wasn't found. Throws on other errors. + + + + Reads the response from the Server Pipe. + + + + + + Leak the current bytes of the response to client. We wait for the full header + set before starting to stream for a variety of impossible-to-change reasons. + + Returns TRUE if response bytes were leaked, false otherwise (e.g. write error) + + + + Get the MIME type (sans Character set or other attributes) from the HTTP Content-Type response header, or String.Empty if missing. + + + + + Peek at number of bytes downloaded thus far. + + + + + DEPRECATED: You should use the Timers object on the Session object instead. + The number of milliseconds between the start of sending the request to the server to the first byte of the server's response + + + + + DEPRECATED: You should use the Timers object on the Session object instead. + The number of milliseconds between the start of sending the request to the server to the last byte of the server's response. + + + + + Was this request forwarded to a gateway? + + + + + Was this request serviced from a reused server connection? + + + + + The HTTP headers of the server's response + + + + + Simple indexer into the Response Headers object + + + + + HTTP Response headers object + + + + + Base class for RequestHeaders and ResponseHeaders + + + + + Text encoding to be used when converting this header object to/from a byte array + + + + + HTTP version (e.g. HTTP/1.1) + + + + + Storage for individual HTTPHeaderItems in this header collection + + + + + Get byte count of this HTTP header instance. + + Byte Count + + + + Number of HTTP headers + + Number of HTTP headers + + + + Enumerator for HTTPHeader storage collection + + Enumerator + + + + Adds a new header containing the specified name and value. + + Name of the header to add. + Value of the header. + Returns the newly-created HTTPHeaderItem. + + + + Determines if the Headers collection contains a header of the specified name, with any value. + + The name of the header to check. (case insensitive) + True, if the header exists. + + + + Returns the Value from a token in the header. Correctly handles double-quoted strings. Requires semicolon for delimiting tokens + + Name of the header + Name of the token + Value of the token if present; otherwise, null + + + + Determines if the Headers collection contains a header of the specified name, and sHeaderValue is part of the Header's value. + + The name of the header to check. (case insensitive) + The partial header value. (case insensitive) + True if the header is found and the value case-insensitively contains the parameter + + + + Determines if the Headers collection contains a header of the specified name, and sHeaderValue=Header's value. Similar to a case-insensitive version of: headers[sHeaderName]==sHeaderValue, although it checks all instances of the named header. + + The name of the header to check. (case insensitive) + The full header value. (case insensitive) + True if the header is found and the value case-insensitively matches the parameter + + + + Removes all headers from the header collection which have the specified name. + + The name of the header to remove. (case insensitive) + + + + Removes a HTTPHeader item from the collection + + The HTTPHeader item to be removed + + + + Renames all headers in the header collection which have the specified name. + + The name of the header to rename. (case insensitive) + The new name for the header. + True if one or more replacements were made. + + + + Gets or sets the value of a header. In the case of Gets, the value of the first header of that name is returned. + If the header does not exist, returns null. + In the case of Sets, the value of the first header of that name is updated. + If the header does not exist, it is added. + + + + + Indexer property. Returns HTTPHeaderItem by index. Throws Exception if index out of bounds + + + + + Clone this HTTPResponseHeaders object and return the result cast to an Object + + The new response headers object, cast to an object + + + + Status code from HTTP Response. If setting, also set HTTPResponseStatus too! + + + + + Status text from HTTP Response (e.g. '200 OK'). + + + + + Constructor for HTTP Response headers object + + + + + Constructor for HTTP Response headers object + + Text encoding to be used for this set of Headers when converting to a byte array + + + + Returns a byte array representing the HTTP headers. + + TRUE if the response status line should be included + TRUE if there should be a trailing \r\n byte sequence included + Byte[] containing the headers + + + + Returns a string containing http headers + + TRUE if the response status line should be included + TRUE if there should be a trailing CRLF included + String containing http headers + + + + Returns a string containing the http headers + + + Returns a string containing http headers with a status line but no trailing CRLF + + + + + Parses a string and assigns the headers parsed to this object + + The header string + TRUE if the operation succeeded, false otherwise + + + + HTTP Request headers object + + + + + Clones the HTTP request headers + + The new HTTPRequestHeaders object, cast to an object + + + + The HTTP Method (e.g. GET, POST, etc) + + + + + Constructor for HTTP Request headers object + + + + + Constructor for HTTP Request headers object + + Text encoding to be used for this set of Headers when converting to a byte array + + + + Parses a string and assigns the headers parsed to this object + + The header string + TRUE if the operation succeeded, false otherwise + + + + Returns a byte array representing the HTTP headers. + + TRUE if the HTTP REQUEST line (method+path+httpversion) should be included + TRUE if there should be a trailing \r\n byte sequence included + TRUE if the SCHEME and HOST should be included in the HTTP REQUEST LINE + The HTTP headers as a byte[] + + + + Returns a string representing the HTTP headers. + + TRUE if the HTTP REQUEST line (method+path+httpversion) should be included + TRUE if there should be a trailing CRLF sequence included + TRUE if the SCHEME and HOST should be included in the HTTP REQUEST LINE (Automatically set to FALSE for CONNECT requests) + The HTTP headers as a string. + + + + Returns a string representing the HTTP headers, without the SCHEME+HOST in the HTTP REQUEST line + + TRUE if the HTTP REQUEST line (method+path+httpversion) should be included + TRUE if there should be a trailing CRLF sequence included + The header string + + + + Returns a string representing the HTTP headers, without the SCHEME+HOST in the HTTP request line, and no trailing CRLF + + The header string + + + + The URI scheme for this request (HTTPS, HTTP, or FTP) + + + + + Username:Password info for FTP URLs. (either null or "user:pass@") + + + + + Get or set the request path as a string + + + + + Get or set the request path as a byte array + + + + + Represents a single HTTP header + + + + + Clones a single HTTP header and returns the clone cast to an object + + HTTPHeader Name: Value pair, cast to an object + + + + The name of the HTTP header + + + + + The value of the HTTP header + + + + + Creates a new HTTP Header item + + Header name + Header value + + + + Return a string of the form "NAME: VALUE" + + "NAME: VALUE" Header string + + + + The SessionTimers class holds timing information about a particular session. + + + + + The time at which the client's HTTP connection to Fiddler was established + + + + + The time at which the request's first Send() to Fiddler completes + + + + + The time at which the request headers were received + + + + + The time at which the request to Fiddler completes (aka RequestLastWrite) + + + + + The time at which the server connection has been established + + + + + The time at which Fiddler begins sending the HTTP request to the server (FiddlerRequestFirstSend) + + + + + The time at which Fiddler has completed sending the HTTP request to the server (FiddlerRequestLastSend). BUG: Should be named "FiddlerEndRequest". + + + + + The time at which Fiddler received the server's headers + + + + + The time at which Fiddler receives the first byte of the server's response (ServerResponseFirstRead) + + + + + The time at which Fiddler has completed receipt of the server's response (ServerResponseLastRead) + + + + + The time at which Fiddler has begun sending the Response to the client (ClientResponseFirstSend) + + + + + The time at which Fiddler has completed sending the Response to the client (ClientResponseLastSend) + + + + + The number of milliseconds spent determining which gateway should be used to handle this request + (Should be mutually exclusive to DNSTime!=0) + + + + + The number of milliseconds spent waiting for DNS + + + + + The number of milliseconds spent waiting for the server TCP/IP connection establishment + + + + + The number of milliseconds elapsed while performing the HTTPS handshake with the server + + + + + Override of ToString shows timer info in a fancy format + + Timing information as a string + + + + Override of ToString shows timer info in a fancy format + + TRUE if the result can contain linebreaks; false if comma-delimited format preferred + Timing information as a string + + + + Attribute used to specify the minimum version of Fiddler compatible with this extension assembly. + + + + + Attribute used to specify the minimum version of Fiddler compatible with this extension assembly. + + The minimal version string (e.g. "2.2.8.8") + + + + Getter for the required version string + + + + + Attribute allowing developer to specify that a class supports the specified Import/Export Format + + + + + Attribute allowing developer to specify that a class supports the specified Import/Export Format + + Shortname of the Format (e.g. WebText XML) + Description of the format + + + + Returns the Shortname for this format + + + + + Returns the Description of this format + + + + + This tuple maps a display string to a Import/Export type + + + + + Textual description of the Format + + + + + Class implementing the format + + + + + Create a new Transcoder Tuple + + Format description + Type implementing this format + + + + ISessionImport allows loading of Session data + + + + + Import Sessions from a data source + + Shortname of the format + Dictionary of options that the Importer class may use + Callback event on which progress is reported or the host may cancel + Array of Session objects imported from source + + + + ISessionExport allows saving of Session data + + + + + Export Sessions to a data store + + Shortname of the format + Array of Sessions being exported + Dictionary of options that the Exporter class may use + Callback event on which progress is reported or the host may cancel + TRUE if the export was successful + + + + EventArgs class for the ISessionImporter and ISessionExporter interface callbacks + + + + + Progress Callback + + Float indicating completion ratio, 0.0 to 1.0. Set to 0 if unknown. + Short string describing current operation, progress, etc + + + + Set to TRUE to request that Import/Export process be aborted as soon as convenient + + + + + The string message of the notification + + + + + The percentage completed + + + + + The core proxy object which accepts connections from clients and creates session objects from those connections + + + + + Hostname if this Proxy Endpoint is terminating HTTPS connections + + + + + Certificate if this Proxy Endpoint is terminating HTTPS connections + + + + + Allow binding to a specific egress adapter: "fiddler.network.egress.ip" + + + + + List of hosts which should bypass the upstream gateway + + + + + Returns a string of information about this instance and the ServerPipe reuse pool + + A multiline string + + + + Watch for relevent changes on the Preferences object + + + + + + + Called whenever Windows reports that the system's NetworkAddress has changed + + + + + + + Called by Windows whenever network availability goes up or down. + + + + + + + Directly inject a session into the Fiddler pipeline, returning a reference to it. + NOTE: This method will THROW any exceptions to its caller. + + HTTP Request Headers + HTTP Request body (or null) + StringDictionary of Session Flags (or null) + The new Session + + + + Directly inject a session into the Fiddler pipeline, returning a reference to it. + NOTE: This method will THROW any exceptions to its caller. + + String representing the HTTP request. If headers only, be sure to end with CRLFCRLF + StringDictionary of Session Flags (or null) + The new session + + + + [DEPRECATED] Directly inject a session into the Fiddler pipeline. + NOTE: This method will THROW any exceptions to its caller. + + + HTTP Request Headers + HTTP Request body (or null) + StringDictionary of Session Flags (or null) + + + + [DEPRECATED] Directly inject a session into the Fiddler pipeline. + NOTE: This method will THROW any exceptions to its caller. + + + String representing the HTTP request. If headers only, be sure to end with CRLFCRLF + StringDictionary of Session Flags (or null) + + + + [DEPRECATED]: This version does no validation of the request data, and doesn't set SessionFlags.RequestGeneratedByFiddler + Send a custom HTTP request to Fiddler's listening endpoint (127.0.0.1:8888 by default). + NOTE: This method will THROW any exceptions to its caller and blocks the current thread. + + + String representing the HTTP request. If headers only, be sure to end with CRLFCRLF + + + + This function, when given a scheme host[:port], returns the gateway information of the proxy to forward requests to. + + URIScheme: use http,https, or ftp + Host for which to return gateway information + IPEndPoint of gateway to use, or NULL + + + + Accept the connection and pass it off to a handler thread + + + + + + Register as the system proxy for WinINET and set the Dynamic registry key for other FiddlerHook + + True if the proxy registration was successful + + + + This method sets up the connectoid list and updates gateway information. Called by the Attach() method, or + called on startup if Fiddler isn't configured to attach automatically. + + + + + Given an address list, walks it until it's able to successfully make a connection. + Used for finding an available Gateway when we have a list to choose from + + A string, e.g. PROXY1:80 + The IP:Port of the first alive endpoint for the specified host/port + + + + Set internal fields pointing at upstream proxies. + + + + + Sets a registry key which indicates that Fiddler is in "Connected" mode. Used by the FiddlerHook Add-on + + TRUE if fiddler is Connected + + + + Detach the proxy by setting the registry keys and sending a Windows Message + + True if the proxy settings were successfully detached + + + + Write a .PAC file that user can point FireFox or some other non-WinINET browser to. This is a crutch + that simplifies using Fiddler with non-WinINET clients. + + + + + + Stop the proxy by closing the socket. + + + + + Start the proxy by binding to the local port and accepting connections + + Port to listen on + TRUE to allow remote connections + + + + + Dispose Fiddler's listening socket + + + + + Clear the pool of Server Pipes. May be called by extensions. + + + + + Assign HTTPS Certificate for this endpoint + + Certificate to return to clients who connect + + + + Generate or find a certificate for this endpoint + + Subject FQDN + TRUE if the certificate could be found/generated, false otherwise + + + + The port on which this instance is listening + + + + + Returns true if Fiddler believes it is currently registered as the Local System proxy + + + + + Assigns a "job" to the Periodic worker, on the schedule specified by iMS. + + The function to run on the timer specified. + Warning: the function is NOT called on the UI thread, so use .Invoke() if needed. + The # of milliseconds to wait between runs + A taskItem which can be used to revokeWork later + + + + Revokes a previously-assigned task from this worker + + + +
+
diff --git a/lib/fiddler/FiddlerCore4.dll b/lib/fiddler/FiddlerCore4.dll new file mode 100644 index 0000000..bc581f1 Binary files /dev/null and b/lib/fiddler/FiddlerCore4.dll differ diff --git a/psake.ps1 b/psake.ps1 new file mode 100644 index 0000000..c645278 --- /dev/null +++ b/psake.ps1 @@ -0,0 +1,43 @@ +# Helper script for those who want to run psake without importing the module. +# Example: +# .\psake.ps1 "default.ps1" "BuildHelloWord" "4.0" + +# Must match parameter definitions for psake.psm1/invoke-psake +# otherwise named parameter binding fails +param( + [Parameter(Position=0,Mandatory=0)] + [string]$buildFile = 'default.ps1', + [Parameter(Position=1,Mandatory=0)] + [string[]]$taskList = @(), + [Parameter(Position=2,Mandatory=0)] + [string]$framework, + [Parameter(Position=3,Mandatory=0)] + [switch]$docs = $false, + [Parameter(Position=4,Mandatory=0)] + [System.Collections.Hashtable]$parameters = @{}, + [Parameter(Position=5, Mandatory=0)] + [System.Collections.Hashtable]$properties = @{}, + [Parameter(Position=6, Mandatory=0)] + [switch]$nologo = $false, + [Parameter(Position=7, Mandatory=0)] + [switch]$help = $false, + [Parameter(Position=8, Mandatory=0)] + [string]$scriptPath = $(Split-Path -parent $MyInvocation.MyCommand.path) +) + +# '[p]sake' is the same as 'psake' but $Error is not polluted +remove-module [p]sake +import-module (join-path $scriptPath psake.psm1) +if ($help) { + Get-Help Invoke-psake -full + return +} + +if (-not(test-path $buildFile)) { + $absoluteBuildFile = (join-path $scriptPath $buildFile) + if (test-path $absoluteBuildFile) { + $buildFile = $absoluteBuildFile + } +} + +invoke-psake $buildFile $taskList $framework $docs $parameters $properties $nologo diff --git a/psake.psm1 b/psake.psm1 new file mode 100644 index 0000000..3c62cb6 --- /dev/null +++ b/psake.psm1 @@ -0,0 +1,697 @@ +# psake +# Copyright (c) 2010 James Kovacs +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +#Requires -Version 2.0 + +#-- Public Module Functions --# + +# .ExternalHelp psake.psm1-help.xml +function Invoke-Task +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)] [string]$taskName + ) + + Assert $taskName ($msgs.error_invalid_task_name) + + $taskKey = $taskName.ToLower() + + if ($currentContext.aliases.Contains($taskKey)) { + $taskName = $currentContext.aliases.$taskKey.Name + $taskKey = $taskName.ToLower() + } + + $currentContext = $psake.context.Peek() + + Assert ($currentContext.tasks.Contains($taskKey)) ($msgs.error_task_name_does_not_exist -f $taskName) + + if ($currentContext.executedTasks.Contains($taskKey)) { return } + + Assert (!$currentContext.callStack.Contains($taskKey)) ($msgs.error_circular_reference -f $taskName) + + $currentContext.callStack.Push($taskKey) + + $task = $currentContext.tasks.$taskKey + + $precondition_is_valid = & $task.Precondition + + if (!$precondition_is_valid) { + Write-ColoredOutput ($msgs.precondition_was_false -f $taskName) -foregroundcolor Cyan + } else { + if ($taskKey -ne 'default') { + + if ($task.PreAction -or $task.PostAction) { + Assert ($task.Action -ne $null) ($msgs.error_missing_action_parameter -f $taskName) + } + + if ($task.Action) { + try { + foreach($childTask in $task.DependsOn) { + Invoke-Task $childTask + } + + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $currentContext.currentTaskName = $taskName + + & $currentContext.taskSetupScriptBlock + + if ($task.PreAction) { + & $task.PreAction + } + + if ($currentContext.config.taskNameFormat -is [ScriptBlock]) { + & $currentContext.config.taskNameFormat $taskName + } else { + Write-ColoredOutput ($currentContext.config.taskNameFormat -f $taskName) -foregroundcolor Cyan + } + + foreach ($variable in $task.requiredVariables) { + Assert ((test-path "variable:$variable") -and ((get-variable $variable).Value -ne $null)) ($msgs.required_variable_not_set -f $variable, $taskName) + } + + & $task.Action + + if ($task.PostAction) { + & $task.PostAction + } + + & $currentContext.taskTearDownScriptBlock + $task.Duration = $stopwatch.Elapsed + } catch { + if ($task.ContinueOnError) { + "-"*70 + Write-ColoredOutput ($msgs.continue_on_error -f $taskName,$_) -foregroundcolor Yellow + "-"*70 + $task.Duration = $stopwatch.Elapsed + } else { + throw $_ + } + } + } else { + # no action was specified but we still execute all the dependencies + foreach($childTask in $task.DependsOn) { + Invoke-Task $childTask + } + } + } else { + foreach($childTask in $task.DependsOn) { + Invoke-Task $childTask + } + } + + Assert (& $task.Postcondition) ($msgs.postcondition_failed -f $taskName) + } + + $poppedTaskKey = $currentContext.callStack.Pop() + Assert ($poppedTaskKey -eq $taskKey) ($msgs.error_corrupt_callstack -f $taskKey,$poppedTaskKey) + + $currentContext.executedTasks.Push($taskKey) +} + +# .ExternalHelp psake.psm1-help.xml +function Exec +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, + [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) + ) + & $cmd + if ($lastexitcode -ne 0) { + throw ("Exec: " + $errorMessage) + } +} + +# .ExternalHelp psake.psm1-help.xml +function Assert +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)]$conditionToCheck, + [Parameter(Position=1,Mandatory=1)]$failureMessage + ) + if (!$conditionToCheck) { + throw ("Assert: " + $failureMessage) + } +} + +# .ExternalHelp psake.psm1-help.xml +function Task +{ + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][string]$name = $null, + [Parameter(Position=1,Mandatory=0)][scriptblock]$action = $null, + [Parameter(Position=2,Mandatory=0)][scriptblock]$preaction = $null, + [Parameter(Position=3,Mandatory=0)][scriptblock]$postaction = $null, + [Parameter(Position=4,Mandatory=0)][scriptblock]$precondition = {$true}, + [Parameter(Position=5,Mandatory=0)][scriptblock]$postcondition = {$true}, + [Parameter(Position=6,Mandatory=0)][switch]$continueOnError = $false, + [Parameter(Position=7,Mandatory=0)][string[]]$depends = @(), + [Parameter(Position=8,Mandatory=0)][string[]]$requiredVariables = @(), + [Parameter(Position=9,Mandatory=0)][string]$description = $null, + [Parameter(Position=10,Mandatory=0)][string]$alias = $null + ) + if ($name -eq 'default') { + Assert (!$action) ($msgs.error_default_task_cannot_have_action) + } + + $newTask = @{ + Name = $name + DependsOn = $depends + PreAction = $preaction + Action = $action + PostAction = $postaction + Precondition = $precondition + Postcondition = $postcondition + ContinueOnError = $continueOnError + Description = $description + Duration = [System.TimeSpan]::Zero + RequiredVariables = $requiredVariables + Alias = $alias + } + + $taskKey = $name.ToLower() + + $currentContext = $psake.context.Peek() + + Assert (!$currentContext.tasks.ContainsKey($taskKey)) ($msgs.error_duplicate_task_name -f $name) + + $currentContext.tasks.$taskKey = $newTask + + if($alias) + { + $aliasKey = $alias.ToLower() + + Assert (!$currentContext.aliases.ContainsKey($aliasKey)) ($msgs.error_duplicate_alias_name -f $alias) + + $currentContext.aliases.$aliasKey = $newTask + } +} + +# .ExternalHelp psake.psm1-help.xml +function Properties { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$properties + ) + $psake.context.Peek().properties += $properties +} + +# .ExternalHelp psake.psm1-help.xml +function Include { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][string]$fileNamePathToInclude + ) + Assert (test-path $fileNamePathToInclude -pathType Leaf) ($msgs.error_invalid_include_path -f $fileNamePathToInclude) + $psake.context.Peek().includes.Enqueue((Resolve-Path $fileNamePathToInclude)); +} + +# .ExternalHelp psake.psm1-help.xml +function FormatTaskName { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)]$format + ) + $psake.context.Peek().config.taskNameFormat = $format +} + +# .ExternalHelp psake.psm1-help.xml +function TaskSetup { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$setup + ) + $psake.context.Peek().taskSetupScriptBlock = $setup +} + +# .ExternalHelp psake.psm1-help.xml +function TaskTearDown { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][scriptblock]$teardown + ) + $psake.context.Peek().taskTearDownScriptBlock = $teardown +} + +# .ExternalHelp psake.psm1-help.xml +function Framework { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=1)][string]$framework + ) + $psake.context.Peek().config.framework = $framework +} + +# .ExternalHelp psake.psm1-help.xml +function Invoke-psake { + [CmdletBinding()] + param( + [Parameter(Position = 0, Mandatory = 0)][string] $buildFile, + [Parameter(Position = 1, Mandatory = 0)][string[]] $taskList = @(), + [Parameter(Position = 2, Mandatory = 0)][string] $framework, + [Parameter(Position = 3, Mandatory = 0)][switch] $docs = $false, + [Parameter(Position = 4, Mandatory = 0)][hashtable] $parameters = @{}, + [Parameter(Position = 5, Mandatory = 0)][hashtable] $properties = @{}, + [Parameter(Position = 6, Mandatory = 0)][switch] $nologo = $false + ) + try { + if (-not $nologo) { + "psake version {0}`nCopyright (c) 2010 James Kovacs`n" -f $psake.version + } + + # If the default.ps1 file exists and the given "buildfile" isn 't found assume that the given + # $buildFile is actually the target Tasks to execute in the default.ps1 script. + if ($buildFile -and !(test-path $buildFile -pathType Leaf) -and (test-path $psake.config_default.buildFileName -pathType Leaf)) { + $taskList = $buildFile.Split(', ') + $buildFile = $psake.config_default.buildFileName + } + + # Execute the build file to set up the tasks and defaults + Assert (test-path $buildFile -pathType Leaf) ($msgs.error_build_file_not_found -f $buildFile) + + $psake.build_script_file = get-item $buildFile + $psake.build_script_dir = $psake.build_script_file.DirectoryName + $psake.build_success = $false + + $psake.context.push(@{ + "taskSetupScriptBlock" = {}; + "taskTearDownScriptBlock" = {}; + "executedTasks" = new-object System.Collections.Stack; + "callStack" = new-object System.Collections.Stack; + "originalEnvPath" = $env:path; + "originalDirectory" = get-location; + "originalErrorActionPreference" = $global:ErrorActionPreference; + "tasks" = @{}; + "aliases" = @{}; + "properties" = @(); + "includes" = new-object System.Collections.Queue; + "config" = Create-ConfigurationForNewContext $buildFile $framework + }) + + Load-Configuration $psake.build_script_dir + + Load-Modules + + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + + set-location $psake.build_script_dir + + $frameworkOldValue = $framework + . $psake.build_script_file.FullName + + $currentContext = $psake.context.Peek() + + if ($framework -ne $frameworkOldValue) { + write-coloredoutput $msgs.warning_deprecated_framework_variable -foregroundcolor Yellow + $currentContext.config.framework = $framework + } + + if ($docs) { + Write-Documentation + Cleanup-Environment + return + } + + Configure-BuildEnvironment + + while ($currentContext.includes.Count -gt 0) { + $includeFilename = $currentContext.includes.Dequeue() + . $includeFilename + } + + foreach ($key in $parameters.keys) { + if (test-path "variable:\$key") { + set-item -path "variable:\$key" -value $parameters.$key | out-null + } else { + new-item -path "variable:\$key" -value $parameters.$key | out-null + } + } + + # The initial dot (.) indicates that variables initialized/modified in the propertyBlock are available in the parent scope. + foreach ($propertyBlock in $currentContext.properties) { + . $propertyBlock + } + + foreach ($key in $properties.keys) { + if (test-path "variable:\$key") { + set-item -path "variable:\$key" -value $properties.$key | out-null + } + } + + # Execute the list of tasks or the default task + if ($taskList) { + foreach ($task in $taskList) { + invoke-task $task + } + } elseif ($currentContext.tasks.default) { + invoke-task default + } else { + throw $msgs.error_no_default_task + } + + Write-ColoredOutput ("`n" + $msgs.build_success + "`n") -foregroundcolor Green + + Write-TaskTimeSummary $stopwatch.Elapsed + + $psake.build_success = $true + } catch { + $currentConfig = Get-CurrentConfigurationOrDefault + if ($currentConfig.verboseError) { + $error_message = "{0}: An Error Occurred. See Error Details Below: `n" -f (Get-Date) + $error_message += ("-" * 70) + "`n" + $error_message += Resolve-Error $_ + $error_message += ("-" * 70) + "`n" + $error_message += "Script Variables" + "`n" + $error_message += ("-" * 70) + "`n" + $error_message += get-variable -scope script | format-table | out-string + } else { + # ($_ | Out-String) gets error messages with source information included. + $error_message = "{0}: An Error Occurred: `n{1}" -f (Get-Date), ($_ | Out-String) + } + + $psake.build_success = $false + + if (!$psake.run_by_psake_build_tester) { + # if we are running in a nested scope (i.e. running a psake script from a psake script) then we need to re-throw the exception + # so that the parent script will fail otherwise the parent script will report a successful build + $inNestedScope = ($psake.context.count -gt 1) + if ( $inNestedScope ) { + throw $_ + } else { + Write-ColoredOutput $error_message -foregroundcolor Red + } + + } + } finally { + Cleanup-Environment + } +} + +#-- Private Module Functions --# +function Write-ColoredOutput { + param( + [string] $message, + [System.ConsoleColor] $foregroundcolor + ) + + $currentConfig = Get-CurrentConfigurationOrDefault + if ($currentConfig.coloredOutput -eq $true) { + if (($Host.UI -ne $null) -and ($Host.UI.RawUI -ne $null)) { + $previousColor = $Host.UI.RawUI.ForegroundColor + $Host.UI.RawUI.ForegroundColor = $foregroundcolor + } + } + + $message + + if ($previousColor -ne $null) { + $Host.UI.RawUI.ForegroundColor = $previousColor + } +} + +function Load-Modules { + $currentConfig = $psake.context.peek().config + if ($currentConfig.modules) { + $currentConfig.modules | foreach { + resolve-path $_ | foreach { + "Loading module: $_" + $module = import-module $_ -passthru + if (!$module) { + throw ($msgs.error_loading_module -f $_.Name) + } + } + } + "" + } +} + +function Load-Configuration { + param( + [string] $configdir = $PSScriptRoot + ) + + $psakeConfigFilePath = (join-path $configdir "psake-config.ps1") + + if (test-path $psakeConfigFilePath -pathType Leaf) { + try { + $config = Get-CurrentConfigurationOrDefault + . $psakeConfigFilePath + } catch { + throw "Error Loading Configuration from psake-config.ps1: " + $_ + } + } +} + +function Get-CurrentConfigurationOrDefault() { + if ($psake.context.count -gt 0) { + return $psake.context.peek().config + } else { + return $psake.config_default + } +} + +function Create-ConfigurationForNewContext { + param( + [string] $buildFile, + [string] $framework + ) + + $previousConfig = Get-CurrentConfigurationOrDefault + + $config = new-object psobject -property @{ + buildFileName = $previousConfig.buildFileName; + framework = $previousConfig.framework; + taskNameFormat = $previousConfig.taskNameFormat; + verboseError = $previousConfig.verboseError; + coloredOutput = $previousConfig.coloredOutput; + modules = $previousConfig.modules + } + + if ($framework) { + $config.framework = $framework; + } + + if ($buildFile) { + $config.buildFileName = $buildFile; + } + + return $config +} + +function Configure-BuildEnvironment { + $framework = $psake.context.peek().config.framework + if ($framework.Length -ne 3 -and $framework.Length -ne 6) { + throw ($msgs.error_invalid_framework -f $framework) + } + $versionPart = $framework.Substring(0, 3) + $bitnessPart = $framework.Substring(3) + $versions = $null + switch ($versionPart) { + '1.0' { + $versions = @('v1.0.3705') + } + '1.1' { + $versions = @('v1.1.4322') + } + '2.0' { + $versions = @('v2.0.50727') + } + '3.0' { + $versions = @('v2.0.50727') + } + '3.5' { + $versions = @('v3.5', 'v2.0.50727') + } + '4.0' { + $versions = @('v4.0.30319') + } + default { + throw ($msgs.error_unknown_framework -f $versionPart, $framework) + } + } + + $bitness = 'Framework' + if ($versionPart -ne '1.0' -and $versionPart -ne '1.1') { + switch ($bitnessPart) { + 'x86' { + $bitness = 'Framework' + } + 'x64' { + $bitness = 'Framework64' + } + { [string]::IsNullOrEmpty($_) } { + $ptrSize = [System.IntPtr]::Size + switch ($ptrSize) { + 4 { + $bitness = 'Framework' + } + 8 { + $bitness = 'Framework64' + } + default { + throw ($msgs.error_unknown_pointersize -f $ptrSize) + } + } + } + default { + throw ($msgs.error_unknown_bitnesspart -f $bitnessPart, $framework) + } + } + } + $frameworkDirs = $versions | foreach { "$env:windir\Microsoft.NET\$bitness\$_\" } + + $frameworkDirs | foreach { Assert (test-path $_ -pathType Container) ($msgs.error_no_framework_install_dir_found -f $_)} + + $env:path = ($frameworkDirs -join ";") + ";$env:path" + # if any error occurs in a PS function then "stop" processing immediately + # this does not effect any external programs that return a non-zero exit code + $global:ErrorActionPreference = "Stop" +} + +function Cleanup-Environment { + if ($psake.context.Count -gt 0) { + $currentContext = $psake.context.Peek() + $env:path = $currentContext.originalEnvPath + Set-Location $currentContext.originalDirectory + $global:ErrorActionPreference = $currentContext.originalErrorActionPreference + [void] $psake.context.Pop() + } +} + +# borrowed from Jeffrey Snover http://blogs.msdn.com/powershell/archive/2006/12/07/resolve-error.aspx +function Resolve-Error($ErrorRecord = $Error[0]) { + $error_message = "`nErrorRecord:{0}ErrorRecord.InvocationInfo:{1}Exception:{2}" + $formatted_errorRecord = $ErrorRecord | format-list * -force | out-string + $formatted_invocationInfo = $ErrorRecord.InvocationInfo | format-list * -force | out-string + $formatted_exception = "" + $Exception = $ErrorRecord.Exception + for ($i = 0; $Exception; $i++, ($Exception = $Exception.InnerException)) { + $formatted_exception += ("$i" * 70) + "`n" + $formatted_exception += $Exception | format-list * -force | out-string + $formatted_exception += "`n" + } + + return $error_message -f $formatted_errorRecord, $formatted_invocationInfo, $formatted_exception +} + +function Write-Documentation { + $currentContext = $psake.context.Peek() + + if ($currentContext.tasks.default) { + $defaultTaskDependencies = $currentContext.tasks.default.DependsOn + } else { + $defaultTaskDependencies = @() + } + + $currentContext.tasks.Keys | foreach-object { + if ($_ -eq "default") { + return + } + + $task = $currentContext.tasks.$_ + new-object PSObject -property @{ + Name = $task.Name; + Description = $task.Description; + "Depends On" = $task.DependsOn -join ", " + Default = if ($defaultTaskDependencies -contains $task.Name) { $true } + } + } | sort 'Name' | format-table -autoSize -property Name,Description,"Depends On",Default +} + +function Write-TaskTimeSummary($invokePsakeDuration) { + "-" * 70 + "Build Time Report" + "-" * 70 + $list = @() + $currentContext = $psake.context.Peek() + while ($currentContext.executedTasks.Count -gt 0) { + $taskKey = $currentContext.executedTasks.Pop() + $task = $currentContext.tasks.$taskKey + if ($taskKey -eq "default") { + continue + } + $list += new-object PSObject -property @{ + Name = $task.Name; + Duration = $task.Duration + } + } + [Array]::Reverse($list) + $list += new-object PSObject -property @{ + Name = "Total:"; + Duration = $invokePsakeDuration + } + # using "out-string | where-object" to filter out the blank line that format-table prepends + $list | format-table -autoSize -property Name,Duration | out-string -stream | where-object { $_ } +} + +DATA msgs { +convertfrom-stringdata @' + error_invalid_task_name = Task name should not be null or empty string. + error_task_name_does_not_exist = Task {0} does not exist. + error_circular_reference = Circular reference found for task {0}. + error_missing_action_parameter = Action parameter must be specified when using PreAction or PostAction parameters for task {0}. + error_corrupt_callstack = Call stack was corrupt. Expected {0}, but got {1}. + error_invalid_framework = Invalid .NET Framework version, {0} specified. + error_unknown_framework = Unknown .NET Framework version, {0} specified in {1}. + error_unknown_pointersize = Unknown pointer size ({0}) returned from System.IntPtr. + error_unknown_bitnesspart = Unknown .NET Framework bitness, {0}, specified in {1}. + error_no_framework_install_dir_found = No .NET Framework installation directory found at {0}. + error_bad_command = Error executing command {0}. + error_default_task_cannot_have_action = 'default' task cannot specify an action. + error_duplicate_task_name = Task {0} has already been defined. + error_duplicate_alias_name = Alias {0} has already been defined. + error_invalid_include_path = Unable to include {0}. File not found. + error_build_file_not_found = Could not find the build file {0}. + error_no_default_task = 'default' task required. + error_loading_module = Error loading module {0}. + warning_deprecated_framework_variable = Warning: Using global variable $framework to set .NET framework version used is deprecated. Instead use Framework function or configuration file psake-config.ps1. + required_variable_not_set = Variable {0} must be set to run task {1}. + postcondition_failed = Postcondition failed for task {0}. + precondition_was_false = Precondition was false, not executing task {0}. + continue_on_error = Error in task {0}. {1} + build_success = Build Succeeded! +'@ +} + +import-localizeddata -bindingvariable msgs -erroraction silentlycontinue + +$script:psake = @{} +$psake.version = "4.1.0" # contains the current version of psake +$psake.context = new-object system.collections.stack # holds onto the current state of all variables +$psake.run_by_psake_build_tester = $false # indicates that build is being run by psake-BuildTester +$psake.config_default = new-object psobject -property @{ + buildFileName = "default.ps1"; + framework = "4.0"; + taskNameFormat = "Executing {0}"; + verboseError = $false; + coloredOutput = $true; + modules = $null; +} # contains default configuration, can be overriden in psake-config.ps1 in directory with psake.psm1 or in directory with current build script + +$psake.build_success = $false # indicates that the current build was successful +$psake.build_script_file = $null # contains a System.IO.FileInfo for the current build script +$psake.build_script_dir = "" # contains a string with fully-qualified path to current build script + +Load-Configuration + +export-modulemember -function invoke-psake, invoke-task, task, properties, include, formattaskname, tasksetup, taskteardown, framework, assert, exec -variable psake diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..bde4d9e --- /dev/null +++ b/readme.md @@ -0,0 +1,66 @@ +S3Emulator +============== +[http://github.com/yadazula/S3Emulator][0] + +S3Emulator is a lightweight server which mimics the services of Amazon S3. It can be useful for development and testing porpuses. +By reducing network traffic, it saves both the time and the money. + +Supported Operations +-------------------- +[GET Service][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTServiceGET.html] +[PUT Bucket][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketPUT.html] +[DELETE Bucket][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketDELETE.html] +[HEAD Bucket][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketHEAD.html] +[GET Bucket(List Objects)][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTBucketGET.html] +[PUT Object][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html] +[GET Object][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html] +[DELETE Object][http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectDELETE.html] + +How to use it ? +--------------- +Download the application from [here][1]. Open a command promt window and just enter "S3Emulator" +When started with default options, all the requests made to "s3.amazonaws.com" will be redirected to S3Emulator. +You can see the full list of options by entering : "S3Emulator -help" + +Options +------- +* Service + Address of s3 service that will be emulated. + Default: s3.amazonaws.com + +* Host + The hostname to use for the http listener. + Default: localhost + +* HostPort + The port to use for the http listener. + Default: 8878 + +* EnableProxy + Proxy is used for redirecting requests to S3Emulator's http listener and supporting [subdomain style bucket names][2]. + If you disable proxy, you need to use Request-URI syle bucket names. + [FiddlerCore][3] is used for proxy operations. + Default: true + +* ProxyPort + The port to use for the proxy. + Default: 8877 + +* Directory + The directory for the storage operations. + [RavenDB][4] is used for persistance. + Default: ~\Data + +* InMemory + If set to true, all storage operations will be in memory. + Default: false + +* MaxBPS + Set maximum bytes per second. Can be used for bandwidth throttling. + Default: infinite + +[0]: http://github.com/yadazula/S3Emulator "S3Emulator on Github" +[1]: http://github.com/yadazula/S3Emulator/downloads "download" +[2]: http://docs.amazonwebservices.com/AmazonS3/latest/dev/VirtualHosting.html#VirtualHostingSpecifyBucket +[3]: http://www.fiddler2.com/Fiddler/Core/ +[4]: http://ravendb.net \ No newline at end of file diff --git a/src/S3Emulator.Sample/Program.cs b/src/S3Emulator.Sample/Program.cs new file mode 100644 index 0000000..23f82b0 --- /dev/null +++ b/src/S3Emulator.Sample/Program.cs @@ -0,0 +1,150 @@ +using System; +using System.IO; +using System.Text; +using Amazon; +using Amazon.S3; +using Amazon.S3.Model; +using S3Emulator.Config; +using S3Emulator.IO; +using S3Emulator.Server; + +namespace S3Emulator.Sample +{ + class Program + { + private const int HostPort = 8878; + private const int ProxyPort = 8877; + private const bool IsProxyEnabled = true; + private const string ServiceUrl = "s3.amazonaws.com"; + private const string AwsAccessKey = "foo"; + private const string AwsSecretAccessKey = "bar"; + private const string Bucket = "bucket1"; + private const string S3ObjectKey = "key1"; + + static void Main() + { + var s3Server = CreateS3Server(); + s3Server.Start(); + + var s3Client = CreateS3Client(); + + try + { + CreateBucket(s3Client, Bucket); + + PutObject(s3Client, Bucket, S3ObjectKey); + + ListObjects(s3Client, Bucket); + + GetObject(s3Client, Bucket, S3ObjectKey); + + DeleteObject(s3Client, Bucket, S3ObjectKey); + + DeleteBucket(s3Client, Bucket); + } + catch (Exception exception) + { + Console.WriteLine(exception); + } + finally + { + s3Client.Dispose(); + s3Server.Dispose(); + } + + Console.WriteLine("Finished."); + Console.Read(); + } + + private static S3Server CreateS3Server() + { + var s3Configuration = new S3Configuration + { + ServiceUrl = ServiceUrl, + Host = "localhost", + HostPort = HostPort, + ProxyPort = ProxyPort, + IsProxyEnabled = IsProxyEnabled, + DataDirectory = "Data", + RunInMemory = true, + MaxBytesPerSecond = ThrottledStream.Infinite + }; + + var s3Server = new S3Server(s3Configuration); + return s3Server; + } + + private static AmazonS3 CreateS3Client() + { + var config = new AmazonS3Config() + .WithCommunicationProtocol(Protocol.HTTP) + .WithServiceURL(ServiceUrl + ":" + HostPort); + + var client = AWSClientFactory.CreateAmazonS3Client(AwsAccessKey, AwsSecretAccessKey, config); + return client; + } + + private static void CreateBucket(AmazonS3 s3Client, string bucket) + { + var putBucketRequest = new PutBucketRequest { BucketName = bucket }; + s3Client.PutBucket(putBucketRequest); + } + + private static void DeleteBucket(AmazonS3 s3Client, string bucket) + { + var deleteBucketRequest = new DeleteBucketRequest { BucketName = bucket }; + s3Client.DeleteBucket(deleteBucketRequest); + } + + private static void PutObject(AmazonS3 s3Client, string bucket, string key) + { + var putObjectRequest = new PutObjectRequest(); + putObjectRequest.WithBucketName(bucket) + .WithKey(key) + .WithContentType("text/plain") + .WithContentBody(key); + + var objectResponse = s3Client.PutObject(putObjectRequest); + objectResponse.Dispose(); + } + + private static void ListObjects(AmazonS3 s3Client, string bucket) + { + var request = new ListObjectsRequest(); + request.WithBucketName(bucket) + .WithPrefix("key") + .WithMaxKeys(4); + do + { + ListObjectsResponse response = s3Client.ListObjects(request); + + if (response.IsTruncated) + { + request.Marker = response.NextMarker; + } + else + { + request = null; + } + } while (request != null); + } + + private static void GetObject(AmazonS3 s3Client, string bucket, string key) + { + var getObjectRequest = new GetObjectRequest().WithBucketName(bucket).WithKey(key); + using (var getObjectResponse = s3Client.GetObject(getObjectRequest)) + { + var memoryStream = new MemoryStream(); + getObjectResponse.ResponseStream.CopyTo(memoryStream); + var content = Encoding.Default.GetString(memoryStream.ToArray()); + Console.WriteLine(content); + } + } + + private static void DeleteObject(AmazonS3 s3Client, string bucket, string key) + { + var deleteObjectRequest = new DeleteObjectRequest().WithBucketName(bucket).WithKey(key); + s3Client.DeleteObject(deleteObjectRequest); + } + } +} diff --git a/src/S3Emulator.Sample/Properties/AssemblyInfo.cs b/src/S3Emulator.Sample/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3f7b2da --- /dev/null +++ b/src/S3Emulator.Sample/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("S3Emulator.Sample")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("S3Emulator.Sample")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a9cc60de-de0c-4cfe-a411-ae8a9389af13")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/S3Emulator.Sample/S3Emulator.Sample.csproj b/src/S3Emulator.Sample/S3Emulator.Sample.csproj new file mode 100644 index 0000000..4ba0b4d --- /dev/null +++ b/src/S3Emulator.Sample/S3Emulator.Sample.csproj @@ -0,0 +1,124 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {F7199CDF-7938-4FA3-8098-B2CEB34AFC81} + Exe + Properties + S3Emulator.Sample + S3Emulator.Sample + v4.0 + + + 512 + + + true + bin\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + false + + + bin\Release\ + TRACE + true + pdbonly + AnyCPU + prompt + false + false + + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\AsyncCtpLibrary.dll + + + False + ..\..\packages\AWSSDK.1.4.9.0\lib\AWSSDK.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\BouncyCastle.Crypto.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Esent.Interop.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\ICSharpCode.NRefactory.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.Contrib.Spatial.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.Contrib.SpellChecker.dll + + + ..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll + + + ..\..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Abstractions.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.Embedded.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.Lightweight.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.MvcIntegration.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Database.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Munin.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Storage.Esent.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Storage.Managed.dll + + + + + + + + + + + + + + + + + + + + + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030} + S3Emulator + + + + + \ No newline at end of file diff --git a/src/S3Emulator.Sample/app.config b/src/S3Emulator.Sample/app.config new file mode 100644 index 0000000..cb2586b --- /dev/null +++ b/src/S3Emulator.Sample/app.config @@ -0,0 +1,3 @@ + + + diff --git a/src/S3Emulator.Sample/packages.config b/src/S3Emulator.Sample/packages.config new file mode 100644 index 0000000..c778339 --- /dev/null +++ b/src/S3Emulator.Sample/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/S3Emulator/Config/S3Configuration.cs b/src/S3Emulator/Config/S3Configuration.cs new file mode 100644 index 0000000..0e922b5 --- /dev/null +++ b/src/S3Emulator/Config/S3Configuration.cs @@ -0,0 +1,14 @@ +namespace S3Emulator.Config +{ + public class S3Configuration + { + public string ServiceUrl { get; set; } + public string Host { get; set; } + public int HostPort { get; set; } + public int ProxyPort { get; set; } + public bool IsProxyEnabled { get; set; } + public string DataDirectory { get; set; } + public bool RunInMemory { get; set; } + public long MaxBytesPerSecond { get; set; } + } +} \ No newline at end of file diff --git a/src/S3Emulator/IO/StreamExtensions.cs b/src/S3Emulator/IO/StreamExtensions.cs new file mode 100644 index 0000000..f0b4942 --- /dev/null +++ b/src/S3Emulator/IO/StreamExtensions.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace S3Emulator.IO +{ + public static class StreamExtensions + { + public static Stream Copy(this Stream stream, long maxBytesPerSecond) + { + var memoryStream = new MemoryStream(); + var throttledStream = new ThrottledStream(memoryStream, maxBytesPerSecond); + + stream.Position = 0; + stream.CopyTo(throttledStream); + return memoryStream; + } + + public static string GenerateMD5CheckSum(this Stream stream) + { + using (MD5 md5 = new MD5CryptoServiceProvider()) + { + var hash = md5.ComputeHash(stream); + var contentMd5 = BitConverter.ToString(hash).Replace("-", ""); + return contentMd5; + } + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/IO/ThrottledStream.cs b/src/S3Emulator/IO/ThrottledStream.cs new file mode 100644 index 0000000..ee0baa2 --- /dev/null +++ b/src/S3Emulator/IO/ThrottledStream.cs @@ -0,0 +1,259 @@ +using System; +using System.IO; +using System.Threading; + +namespace S3Emulator.IO +{ + public class ThrottledStream : Stream + { + public const long Infinite = 0; + + private readonly Stream sourceStream; + + private long maxBytesPerSecond; + + private long byteCount; + + private long initialTicks; + + protected long CurrentTicks + { + get + { + return DateTime.UtcNow.Ticks; + } + } + + public long MaxBytesPerSecond + { + get + { + return maxBytesPerSecond; + } + set + { + if (MaxBytesPerSecond != value) + { + maxBytesPerSecond = value; + Reset(); + } + } + } + + public override bool CanRead + { + get + { + return sourceStream.CanRead; + } + } + + public override bool CanSeek + { + get + { + return sourceStream.CanSeek; + } + } + + public override bool CanWrite + { + get + { + return sourceStream.CanWrite; + } + } + + public override long Length + { + get + { + return sourceStream.Length; + } + } + + public override long Position + { + get + { + return sourceStream.Position; + } + set + { + sourceStream.Position = value; + } + } + + public ThrottledStream(Stream sourceStream) + : this(sourceStream, Infinite) + { + } + + public ThrottledStream(Stream sourceStream, long maxBytesPerSecond) + { + if (sourceStream == null) + { + throw new ArgumentNullException("sourceStream"); + } + + if (maxBytesPerSecond < 0) + { + throw new ArgumentOutOfRangeException("maxBytesPerSecond", maxBytesPerSecond, "The maximum number of bytes per second can't be negative."); + } + + this.sourceStream = sourceStream; + this.maxBytesPerSecond = maxBytesPerSecond; + initialTicks = CurrentTicks; + byteCount = 0; + } + + public override int Read(byte[] buffer, int offset, int count) + { + var readCount = sourceStream.Read(buffer, offset, count); + Throttle(readCount); + + return readCount; + } + + public override void Write(byte[] buffer, int offset, int count) + { + Throttle(count); + sourceStream.Write(buffer, offset, count); + } + + public override void Flush() + { + sourceStream.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return sourceStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + sourceStream.SetLength(value); + } + + public override bool CanTimeout + { + get + { + return sourceStream.CanTimeout; + } + } + + public override int ReadTimeout + { + get + { + return sourceStream.ReadTimeout; + } + set + { + sourceStream.ReadTimeout = value; + } + } + + public override int WriteTimeout + { + get + { + return sourceStream.WriteTimeout; + } + set + { + sourceStream.WriteTimeout = value; + } + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return sourceStream.BeginRead(buffer, offset, count, callback, state); + } + + public override int EndRead(IAsyncResult asyncResult) + { + return sourceStream.EndRead(asyncResult); + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) + { + return sourceStream.BeginWrite(buffer, offset, count, callback, state); + } + + public override void EndWrite(IAsyncResult asyncResult) + { + sourceStream.EndWrite(asyncResult); + } + + public override int ReadByte() + { + return sourceStream.ReadByte(); + } + + public override void WriteByte(byte value) + { + sourceStream.WriteByte(value); + } + + public override void Close() + { + sourceStream.Close(); + } + + public override string ToString() + { + return sourceStream.ToString(); + } + + protected void Throttle(int bufferSizeInBytes) + { + if (maxBytesPerSecond <= 0 || bufferSizeInBytes <= 0) + { + return; + } + + byteCount += bufferSizeInBytes; + var elapsedMilliseconds = (CurrentTicks - initialTicks) / TimeSpan.TicksPerMillisecond; + if (elapsedMilliseconds == 0) elapsedMilliseconds = 1; + var bps = byteCount * 1000L / elapsedMilliseconds; + + if (bps < maxBytesPerSecond) + { + return; + } + + var wakeElapsed = byteCount * 1000L / maxBytesPerSecond; + var toSleep = (int)(wakeElapsed - elapsedMilliseconds); + + if (toSleep <= 1) + { + return; + } + + try + { + Thread.Sleep(toSleep); + } + catch (ThreadAbortException) + { + } + finally + { + Reset(); + } + } + + protected void Reset() + { + long difference = CurrentTicks - initialTicks; + if (difference > 1000) + { + byteCount = 0; + initialTicks = CurrentTicks; + } + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Model/Bucket.cs b/src/S3Emulator/Model/Bucket.cs new file mode 100644 index 0000000..f66710a --- /dev/null +++ b/src/S3Emulator/Model/Bucket.cs @@ -0,0 +1,10 @@ +using System; + +namespace S3Emulator.Model +{ + public class Bucket + { + public string Id { get; set; } + public DateTime CreationDate { get; set; } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Model/S3Object.cs b/src/S3Emulator/Model/S3Object.cs new file mode 100644 index 0000000..fb24958 --- /dev/null +++ b/src/S3Emulator/Model/S3Object.cs @@ -0,0 +1,41 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace S3Emulator.Model +{ + public class S3Object + { + private string bucket; + private string key; + + public string Id { get; set; } + public string ContentMD5 { get; set; } + public string ContentType { get; set; } + public DateTime CreationDate { get; set; } + public long Size { get; set; } + + [JsonIgnore] + public Func Content { get; set; } + + public string Key + { + get { return key; } + set + { + key = value; + Id = bucket + "/" + key; + } + } + + public string Bucket + { + get { return bucket; } + set + { + bucket = value; + Id = bucket + "/" + key; + } + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Program.cs b/src/S3Emulator/Program.cs new file mode 100644 index 0000000..5e4d37c --- /dev/null +++ b/src/S3Emulator/Program.cs @@ -0,0 +1,89 @@ +using System; +using NDesk.Options; +using S3Emulator.Config; +using S3Emulator.IO; +using S3Emulator.Server; + +namespace S3Emulator +{ + class Program + { + static void Main(string[] args) + { + var s3Configuration = GetDefaultConfiguration(); + var shouldStartServer = true; + + OptionSet optionSet = null; + optionSet = new OptionSet + { + {"service=", "Set S3 service address (default: s3.amazonaws.com)", key => s3Configuration.ServiceUrl = key}, + {"host=", "Set host name (default: localhost)", key => s3Configuration.Host = key}, + {"hostport=", "Set host port (default: 8878)", key => s3Configuration.HostPort = Convert.ToInt32(key)}, + {"enableproxy=", "Enable/Disable proxying (default: true)", key => s3Configuration.IsProxyEnabled = Convert.ToBoolean(key)}, + {"proxyport=", "Set proxy port (default: 8877)", key => s3Configuration.ProxyPort = Convert.ToInt32(key)}, + {"directory=", "Data directory (default: ~\\Data)", key => s3Configuration.DataDirectory = key}, + {"inmemory=", "Use in memory storage (default: false)", key => s3Configuration.RunInMemory = Convert.ToBoolean(key)}, + {"maxbps=", "Set maximum bytes per second (default: infinite)", key => s3Configuration.MaxBytesPerSecond = Convert.ToInt64(key)}, + {"help", "Show configuration options", key => { PrintOptions(optionSet); shouldStartServer = false; }}, + }; + + try + { + optionSet.Parse(args); + } + catch (Exception e) + { + Console.WriteLine(e.Message); + PrintOptions(optionSet); + return; + } + + if (shouldStartServer) + { + StartServer(s3Configuration); + } + } + + private static S3Configuration GetDefaultConfiguration() + { + var s3Configuration = new S3Configuration + { + ServiceUrl = "s3.amazonaws.com", + Host = "localhost", + HostPort = 8878, + ProxyPort = 8877, + IsProxyEnabled = true, + DataDirectory = "Data", + MaxBytesPerSecond = ThrottledStream.Infinite + }; + return s3Configuration; + } + + private static void PrintOptions(OptionSet optionSet) + { + Console.WriteLine(); + Console.WriteLine(@"Options :"); + Console.WriteLine(); + optionSet.WriteOptionDescriptions(Console.Out); + Console.WriteLine(); + } + + private static void StartServer(S3Configuration s3Configuration) + { + using (var s3Server = new S3Server(s3Configuration)) + { + s3Server.Start(); + + Console.WriteLine("S3Emulator is started"); + Console.WriteLine("Listening connections at '{0}:{1}'", s3Configuration.Host, s3Configuration.HostPort); + if (s3Configuration.IsProxyEnabled) + { + Console.WriteLine("Proxy is enabled"); + Console.WriteLine("Requests for '{0}' will be redirected to '{1}:{2}'", s3Configuration.ServiceUrl, s3Configuration.Host, s3Configuration.HostPort); + } + Console.WriteLine("Press to stop"); + Console.ReadLine(); + } + } + } +} diff --git a/src/S3Emulator/Properties/AssemblyInfo.cs b/src/S3Emulator/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..4a03ed4 --- /dev/null +++ b/src/S3Emulator/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("S3Emulator")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("S3Emulator")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7382db0d-b9ab-41c9-a07d-a596700acdfe")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/S3Emulator/S3Emulator.csproj b/src/S3Emulator/S3Emulator.csproj new file mode 100644 index 0000000..599fc42 --- /dev/null +++ b/src/S3Emulator/S3Emulator.csproj @@ -0,0 +1,174 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030} + Exe + Properties + S3Emulator + S3Emulator + v4.0 + + + 512 + + + true + bin\Debug\ + DEBUG;TRACE + full + AnyCPU + bin\Debug\S3Emulator.exe.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + MinimumRecommendedRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + false + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + false + + + bin\Release\ + TRACE + true + pdbonly + AnyCPU + bin\Release\S3Emulator.exe.CodeAnalysisLog.xml + true + GlobalSuppressions.cs + prompt + MinimumRecommendedRules.ruleset + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\\Rule Sets + false + ;C:\Program Files (x86)\Microsoft Visual Studio 10.0\Team Tools\Static Analysis Tools\FxCop\\Rules + false + + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\AsyncCtpLibrary.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\BouncyCastle.Crypto.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Esent.Interop.dll + + + ..\..\lib\fiddler\FiddlerCore4.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\ICSharpCode.NRefactory.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.Contrib.Spatial.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.Contrib.SpellChecker.dll + + + False + ..\..\packages\Nancy.0.11.0\lib\net40\Nancy.dll + + + False + ..\..\packages\Nancy.Hosting.Self.0.11.0\lib\net40\Nancy.Hosting.Self.dll + + + ..\..\packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll + + + ..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll + + + ..\..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Abstractions.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.Embedded.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.Lightweight.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.MvcIntegration.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Database.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Munin.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Storage.Esent.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Storage.Managed.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + PreserveNewest + + + + + + \ No newline at end of file diff --git a/src/S3Emulator/Server/Bootstrapper.cs b/src/S3Emulator/Server/Bootstrapper.cs new file mode 100644 index 0000000..b3034ec --- /dev/null +++ b/src/S3Emulator/Server/Bootstrapper.cs @@ -0,0 +1,44 @@ +using System; +using Nancy; +using Raven.Client; +using Raven.Client.Embedded; +using S3Emulator.Config; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; +using S3Emulator.Storage.Indexes; + +namespace S3Emulator.Server +{ + public class Bootstrapper : DefaultNancyBootstrapper, IDisposable + { + + private readonly S3Configuration s3Configuration; + private readonly IDocumentStore documentStore; + + public Bootstrapper(S3Configuration s3Configuration) + { + this.s3Configuration = s3Configuration; + documentStore = new EmbeddableDocumentStore + { + DataDirectory = s3Configuration.DataDirectory, + RunInMemory = s3Configuration.RunInMemory + }; + + documentStore.Initialize(); + Raven.Client.Indexes.IndexCreation.CreateIndexes(typeof(S3Object_Search).Assembly, documentStore); + } + + protected override void ConfigureApplicationContainer(TinyIoC.TinyIoCContainer container) + { + container.Register(documentStore); + container.Register(s3Configuration); + container.Register().AsSingleton(); + container.Register().AsSingleton(); + } + + public void Dispose() + { + documentStore.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Modules/BucketModule.cs b/src/S3Emulator/Server/Modules/BucketModule.cs new file mode 100644 index 0000000..9f7d4af --- /dev/null +++ b/src/S3Emulator/Server/Modules/BucketModule.cs @@ -0,0 +1,89 @@ +using System; +using Nancy; +using S3Emulator.Model; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; + +namespace S3Emulator.Server.Modules +{ + public class BucketModule : NancyModule + { + private readonly IS3Storage storage; + private readonly IS3Responder responder; + + public BucketModule(IS3Storage storage, IS3Responder responder) + { + this.storage = storage; + this.responder = responder; + + Get["/{bucket}"] = x => GetBucket(x.bucket, Request.Method); + Put["/{bucket}"] = x => AddBucket(x.bucket); + Delete["/{bucket}"] = x => DeleteBucket(x.bucket); + } + + private Response AddBucket(string bucketName) + { + var bucket = new Bucket { Id = bucketName, CreationDate = DateTime.UtcNow }; + storage.AddBucket(bucket); + + var response = new Response { StatusCode = HttpStatusCode.OK }; + return response; + } + + private Response DeleteBucket(string bucket) + { + storage.DeleteBucket(bucket); + + var response = new Response { StatusCode = HttpStatusCode.NoContent }; + return response; + } + + private Response GetBucket(string bucket, string method) + { + if (method.ToUpperInvariant() == "HEAD") + { + return CheckBucketExist(bucket); + } + + return ListObjects(bucket); + } + + private Response CheckBucketExist(string bucket) + { + var bucketObject = storage.GetBucket(bucket); + if (bucketObject == null) + { + var responseNotFound = responder.Respond(new BucketNotFound { BucketName = bucket }); + responseNotFound.StatusCode = HttpStatusCode.NotFound; + return responseNotFound; + } + + var response = new Response { StatusCode = HttpStatusCode.OK }; + return response; + } + + private Response ListObjects(string bucket) + { + var bucketObject = storage.GetBucket(bucket); + if (bucketObject == null) + { + var responseNotFound = responder.Respond(new BucketNotFound { BucketName = bucket }); + responseNotFound.StatusCode = HttpStatusCode.NotFound; + return responseNotFound; + } + + var searchRequest = new S3ObjectSearchRequest + { + BucketName = bucket, + Prefix = Request.Query.prefix.HasValue ? Request.Query.prefix : string.Empty, + Delimiter = Request.Query.delimiter.HasValue ? Request.Query.delimiter : string.Empty, + Marker = Request.Query.marker.HasValue ? Request.Query.marker : string.Empty, + MaxKeys = Request.Query.maxkeys.HasValue ? Request.Query.maxkeys : 1000, + }; + + var searchResponse = storage.GetObjects(searchRequest); + var response = responder.Respond(searchResponse); + return response; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Modules/S3ObjectModule.cs b/src/S3Emulator/Server/Modules/S3ObjectModule.cs new file mode 100644 index 0000000..b2d05a7 --- /dev/null +++ b/src/S3Emulator/Server/Modules/S3ObjectModule.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using Nancy; +using S3Emulator.Config; +using S3Emulator.IO; +using S3Emulator.Model; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; + +namespace S3Emulator.Server.Modules +{ + public class S3ObjectModule : NancyModule + { + private readonly S3Configuration configuration; + private readonly IS3Storage storage; + + public S3ObjectModule(S3Configuration configuration, IS3Storage storage, IS3Responder responder) + { + this.configuration = configuration; + this.storage = storage; + + Get["/{bucket}/{key}"] = x => GetObject(x.bucket, x.key); + Put["/{bucket}/{key}"] = x => AddObject(x.bucket, x.key, Request.Body); + Delete["/{bucket}/{key}"] = x => DeleteObject(x.bucket, x.key); + } + + private Response AddObject(string bucket, string key, Stream stream) + { + var content = stream.Copy(configuration.MaxBytesPerSecond); + + var s3Object = new S3Object + { + Bucket = bucket, + Key = key, + ContentType = Request.Headers.ContentType, + CreationDate = DateTime.UtcNow, + Content = () => content, + ContentMD5 = content.GenerateMD5CheckSum(), + Size = content.Length + }; + + storage.AddObject(s3Object); + + var response = new Response { StatusCode = HttpStatusCode.OK }; + response.WithHeader("ETag", string.Format("\"{0}\"", s3Object.ContentMD5)); + return response; + } + + private Response GetObject(string bucket, string key) + { + var s3Object = storage.GetObject(bucket, key); + if (s3Object == null) + { + return new Response { StatusCode = HttpStatusCode.NotFound }; + } + + var stream = s3Object.Content(); + + var response = new Response { StatusCode = HttpStatusCode.OK, ContentType = s3Object.ContentType }; + response.WithHeader("ETag", string.Format("\"{0}\"", s3Object.ContentMD5)); + response.WithHeader("Accept-Ranges", "bytes"); + response.Contents = x => + { + var throttledStream = new ThrottledStream(stream, configuration.MaxBytesPerSecond); + throttledStream.CopyTo(x); + }; + + return response; + } + + private Response DeleteObject(string bucket, string key) + { + storage.DeleteObject(bucket, key); + + var response = new Response { StatusCode = HttpStatusCode.NoContent }; + return response; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Modules/ServiceModule.cs b/src/S3Emulator/Server/Modules/ServiceModule.cs new file mode 100644 index 0000000..c850039 --- /dev/null +++ b/src/S3Emulator/Server/Modules/ServiceModule.cs @@ -0,0 +1,27 @@ +using Nancy; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; + +namespace S3Emulator.Server.Modules +{ + public class ServiceModule : NancyModule + { + private readonly IS3Storage storage; + private readonly IS3Responder responder; + + public ServiceModule(IS3Storage storage, IS3Responder responder) + { + this.storage = storage; + this.responder = responder; + Get["/"] = x => ListBuckets(); + } + + private Response ListBuckets() + { + var bucketList = storage.GetBuckets(); + + var response = responder.Respond(bucketList); + return response; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/BucketNotFound.cs b/src/S3Emulator/Server/Responses/BucketNotFound.cs new file mode 100644 index 0000000..d952c58 --- /dev/null +++ b/src/S3Emulator/Server/Responses/BucketNotFound.cs @@ -0,0 +1,7 @@ +namespace S3Emulator.Server.Responses +{ + public class BucketNotFound + { + public string BucketName { get; set; } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/DynamicXmlBuilder.cs b/src/S3Emulator/Server/Responses/DynamicXmlBuilder.cs new file mode 100644 index 0000000..0fa032a --- /dev/null +++ b/src/S3Emulator/Server/Responses/DynamicXmlBuilder.cs @@ -0,0 +1,393 @@ +#region license +/* DynamicBuilder + * Suspiciously pleasant XML construction API for C# 4, inspired by Ruby's Builder + * http://github.com/mmonteleone/DynamicBuilder + * + * Copyright (C) 2010-2011 Michael Monteleone (http://michaelmonteleone.net) + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#endregion + +using System; +using System.Linq; +using System.Xml.Linq; +using System.Dynamic; +using System.IO; +using System.Xml; +using System.Text; + +namespace S3Emulator.Server.Responses +{ + /// + /// A tiny C# 4 internal-DSL for declaratively generating XML. + /// The generated XML can be used as-is, exported as string content, or as virtually + /// every native .NET XML type for further manipulation/usage/querying. + /// + /// Inpired quite heavily by the Builder library for Ruby http://builder.rubyforge.org/, + /// and made possible thanks to C# 4's dynamic invocation support. + /// + public class DynamicXmlBuilder : DynamicObject + { + private readonly XDocument root = new XDocument(); + private XContainer current; + private XNamespace @namespace; + + /// + /// Returns a lambda as a strongly-typed Action for use by lambda-accepting + /// dynamic dispatch on Xml. Not unequivalent to simply casting the same + /// lambda when passing to Xml, except slightly cleaner syntax. This is only + /// necessary since dynamic calls cannot accept weakly-typed lambdas /sigh + /// + /// + /// passed block, typed as an action + public static Action Fragment(Action fragmentBuilder) + { + if (fragmentBuilder == null) { throw new ArgumentNullException("fragmentBuilder"); } + + return fragmentBuilder; + } + + /// + /// Returns a lambda as a strongly-typed Generic Actions of type dynamic for + /// use by the lambda-accepting dynamic dispatch on Xml. Not unequivalent to + /// simply casting the same lambda when passing to Xml, except slightly cleaner syntax + /// This is only necessary since dynamic calls cannot accept weakly-typed lambdas /sigh + /// + /// + /// passed lambda, typed as an Action<dynamic> + public static Action Fragment(Action fragmentBuilder) + { + if (fragmentBuilder == null) { throw new ArgumentNullException("fragmentBuilder"); } + + return fragmentBuilder; + } + + /// + /// Alternate syntax for generating an XML object via this static factory + /// method instead of expliclty creating a "dynamic" in client code. + /// + /// + /// + public static DynamicXmlBuilder Build(Action builder) + { + if (builder == null) { throw new ArgumentNullException("builder"); } + + var xbuilder = new DynamicXmlBuilder(); + builder(xbuilder); + return xbuilder; + } + + /// + /// Constructs a new Dynamic XML Builder + /// + public DynamicXmlBuilder() + { + current = root; + } + + /// + /// Converts dynamically invoked method calls into nodes. + /// example 1: xml.hello("world") becomes world + /// example 2: xml.hello("world2", new { foo = "bar" }) becomes world + /// + /// invoke member binder + /// args + /// result (always true) + /// + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + result = null; + string tagName = binder.Name; + Tag(tagName, args); + return true; + } + + /// + /// Builds an XML node along with setting its inner content, attributes, and possibly nested nodes + /// Usually No need to call this directly as it's mainly used as the implementation for dynamicaly invoked + /// members on an XML instance. + /// + /// name for node tag + /// text content and/or attributes represented as an anonymous object + /// and/or lambda for generating child nodes + public void Tag(string tagName, params object[] args) + { + if (String.IsNullOrEmpty(tagName)) { throw new ArgumentNullException("tagName"); } + + // allow for naming tags same as reserved Xml methods + // like 'Comment' and 'CData' by prefixing "_" + // escape character on tag name/method call + if (tagName.IndexOf('_') == 0) + tagName = tagName.Substring(1); + + string content = null; + object attributes = null; + Action fragment = null; + + // Analyze all the arguments passed + args.ToList().ForEach(arg => + { + // argument was a delegate for building child nodes + if (arg is Action) + fragment = arg as Action; + else if (arg is Action) + fragment = () => (arg as Action)(this); + + // argument was a string literal + else if (arg is string) + content = arg as string; + + // argument was a value type literal + else if (arg.GetType().IsValueType) + content = arg.ToString(); + + // otherwise, argument is considered to be an anonymous + // object literal which will be reflected into node attributes + else + attributes = arg; + }); + + // make a new element for this Tag() call + var element = new XElement(tagName); + if (@namespace != null) + { + element.Name = @namespace + element.Name.ToString(); + } + current.Add(element); + + // if a fragment delegate was passed for building inner nodes + // capture this element as the new current outer parent + if (fragment != null) + { + current = element; + } + + // add literal string content if there was any + if (!String.IsNullOrEmpty(content)) + { + element.Add(content); + } + // add attributes to the element if they were passed + if (attributes != null) + { + attributes.GetType().GetProperties().ToList().ForEach(prop => + { + // if the attribute was named "xmlns", let's treat it + // like an actual xml namespace and do the right thing by + // applying it as a namespace to the element. + if (prop.Name == "xmlns") + { + @namespace = prop.GetValue(attributes, null) as string; + element.Name = @namespace + tagName; + } + // otherwise, just convert the property name/value to an attribute pair + // on the element + else + { + element.Add(new XAttribute(prop.Name, prop.GetValue(attributes, null))); + } + }); + } + + // if a fragment delegate was passed for building inner nodes + // now go ahead and execute the delegate, and then set the current outer parent + // node back to its original value + if (fragment != null) + { + fragment(); + current = element.Parent; + } + } + + /// + /// Add a literal comment to the XML + /// + /// comment content + public void Comment(string comment) + { + if (String.IsNullOrEmpty(comment)) { throw new ArgumentNullException("comment"); } + + current.Add(new XComment(comment)); + } + + /// + /// Add literal CData content to the XML + /// + /// data + public void CData(string data) + { + if (String.IsNullOrEmpty(data)) { throw new ArgumentNullException("data"); } + + current.Add(new XCData(data)); + } + + /// + /// Add a text node to the XML (not commonly needed) + /// + /// text content + public void Text(string text) + { + if (String.IsNullOrEmpty(text)) { throw new ArgumentNullException("text"); } + + current.Add(new XText(text)); + } + + /// + /// Apply a declaration to the XML + /// + /// XML version + /// XML encoding (currently only supports utf-8 or utf-16) + /// "yes" or "no" + public void Declaration(string version = null, string encoding = null, string standalone = null) + { + root.Declaration = new XDeclaration(version, encoding, standalone); + } + + /// + /// Apply a document type to the XML + /// + /// name of the DTD + /// public identifier for the DTD + /// system identifier for the DTD + /// internal subset for the DTD + public void DocumentType(string name, string publicId = null, string systemId = null, string internalSubset = null) + { + if (String.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } + + root.Add(new XDocumentType(name, publicId, systemId, internalSubset)); + } + + // Convertors for exporting as several useful .NET representations of the XML contnet + #region converters + + /// + /// Implicit conversion to non-indented xml content string + /// + /// + /// + public static implicit operator string(DynamicXmlBuilder dynamicXmlBuilder) + { + return dynamicXmlBuilder.ToString(false); + } + + /// + /// Converts the Xml content to a string + /// + /// whether or not to indent the output + /// + public string ToString(bool indent) + { + // HACK justificiation: + + // The native XDocument.ToString() method never includes the XDeclaration (bug?) + // Thus this below hack for manually writing the document to a stream. + + // Moreover, there's no straightforward/elegant way of getting the XmlWriter to respect + // an XDeclaration's encoding property, so manually inspecting the prop's string content. + + // This current implementation limits an XDeclaration to either utf-8 or utf-16 + // but at least it gets us *that* far. + + // default to utf-8 encoding + Encoding encoding = new UTF8Encoding(false); + // if there was an explicit declaration that asked for utf-16, use UnicodeEncoding instead + if (root.Declaration != null && + !String.IsNullOrEmpty(root.Declaration.Encoding) && + root.Declaration.Encoding.ToLowerInvariant() == "utf-16") + encoding = new UnicodeEncoding(false, false); + + MemoryStream memoryStream = new MemoryStream(); + + XmlWriter xmlWriter = XmlWriter.Create(memoryStream, new XmlWriterSettings + { + Encoding = encoding, + Indent = indent, + CloseOutput = true, + // if "Declaration" not eplicitly set, don't include xml declaration + OmitXmlDeclaration = root.Declaration == null + }); + root.Save(xmlWriter); + xmlWriter.Flush(); + xmlWriter.Close(); + + // convert the xml stream to a string with the proper encoding + if (encoding is UnicodeEncoding) + return Encoding.Unicode.GetString(memoryStream.ToArray()); + else + return Encoding.UTF8.GetString(memoryStream.ToArray()); + } + + /// + /// Exports the Xml content as a Linq-queryable XDocument + /// + /// Linq-queryable XDocument + public XDocument ToXDocument() + { + return root; + } + + /// + /// Exports the Xml content as a Linq-queryable XElement + /// + /// Linq-queryable XElement + public XElement ToXElement() + { + return root.Elements().FirstOrDefault(); + } + + /// + /// Exports the Xml content as a standard XmlDocument + /// + /// XmlDocument + public XmlDocument ToXmlDocument() + { + var xmlDoc = new XmlDocument(); + xmlDoc.Load(root.CreateReader()); + return xmlDoc; + } + + /// + /// Exports the Xml content as a standard XmlNode by returning the + /// first node in the XDocument, excluding the DocumentType if it's set + /// + /// XmlNode + public XmlNode ToXmlNode() + { + if (root.DocumentType != null && root.Nodes().Count() > 1) + return ToXmlDocument().ChildNodes[1] as XmlNode; + else if (root.DocumentType == null && root.Nodes().Count() >= 1) + return ToXmlDocument().FirstChild as XmlNode; + else + return null as XmlNode; + } + + /// + /// Exports the Xml content as a standard XmlElement + /// + /// XmlElement + public XmlElement ToXmlElement() + { + return ToXmlNode() as XmlElement; + } + + #endregion + } + +} diff --git a/src/S3Emulator/Server/Responses/IS3Responder.cs b/src/S3Emulator/Server/Responses/IS3Responder.cs new file mode 100644 index 0000000..ab5a1f8 --- /dev/null +++ b/src/S3Emulator/Server/Responses/IS3Responder.cs @@ -0,0 +1,9 @@ +using Nancy; + +namespace S3Emulator.Server.Responses +{ + public interface IS3Responder + { + Response Respond(TModel model); + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/S3ObjectSearchRequest.cs b/src/S3Emulator/Server/Responses/S3ObjectSearchRequest.cs new file mode 100644 index 0000000..9375410 --- /dev/null +++ b/src/S3Emulator/Server/Responses/S3ObjectSearchRequest.cs @@ -0,0 +1,11 @@ +namespace S3Emulator.Server.Responses +{ + public class S3ObjectSearchRequest + { + public string BucketName { get; set; } + public string Delimiter { get; set; } + public string Marker { get; set; } + public int? MaxKeys { get; set; } + public string Prefix { get; set; } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/S3ObjectSearchResponse.cs b/src/S3Emulator/Server/Responses/S3ObjectSearchResponse.cs new file mode 100644 index 0000000..7dce4ed --- /dev/null +++ b/src/S3Emulator/Server/Responses/S3ObjectSearchResponse.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using S3Emulator.Model; + +namespace S3Emulator.Server.Responses +{ + public class S3ObjectSearchResponse + { + public string BucketName { get; set; } + public string Delimiter { get; set; } + public string Marker { get; set; } + public int? MaxKeys { get; set; } + public string Prefix { get; set; } + public bool IsTruncated { get; set; } + public IEnumerable S3Objects { get; set; } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/S3XmlResponder.cs b/src/S3Emulator/Server/Responses/S3XmlResponder.cs new file mode 100644 index 0000000..50f0409 --- /dev/null +++ b/src/S3Emulator/Server/Responses/S3XmlResponder.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Nancy; +using S3Emulator.Model; +using S3Emulator.Server.Responses.Serializers; + +namespace S3Emulator.Server.Responses +{ + public class S3XmlResponder : IS3Responder + { + public Response Respond(T t) + { + var serializer = GetSerializer(t); + var response = new Response { ContentType = "application/xml", Contents = (stream => serializer.Serialize(t, stream)) }; + return response; + } + + protected IS3Serializer GetSerializer(object o) + { + if (o is IEnumerable) + return new BucketListSerializer(); + + if (o is S3ObjectSearchResponse) + return new S3ObjectSearchResponeSerializer(); + + if (o is BucketNotFound) + return new BucketNotFoundSerializer(); + + return new NullSerializer(); + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/Serializers/AbstractS3Serializer.cs b/src/S3Emulator/Server/Responses/Serializers/AbstractS3Serializer.cs new file mode 100644 index 0000000..a9d8f45 --- /dev/null +++ b/src/S3Emulator/Server/Responses/Serializers/AbstractS3Serializer.cs @@ -0,0 +1,18 @@ +using System.IO; + +namespace S3Emulator.Server.Responses.Serializers +{ + public abstract class AbstractS3Serializer : IS3Serializer + { + void IS3Serializer.Serialize(T t, Stream stream) + { + var model = (TModel)(object)t; + var response = SerializeInternal(model); + var sw = new StreamWriter(stream); + sw.Write(response); + sw.Flush(); + } + + protected abstract string SerializeInternal(TModel model); + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/Serializers/BucketListSerializer.cs b/src/S3Emulator/Server/Responses/Serializers/BucketListSerializer.cs new file mode 100644 index 0000000..a07257c --- /dev/null +++ b/src/S3Emulator/Server/Responses/Serializers/BucketListSerializer.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using S3Emulator.Model; + +namespace S3Emulator.Server.Responses.Serializers +{ + public class BucketListSerializer : AbstractS3Serializer> + { + protected override string SerializeInternal(IEnumerable bucketList) + { + dynamic builder = new DynamicXmlBuilder(); + builder.Declaration(); + builder.ListAllMyBucketsResult(new { xmlns = "http://s3.amazonaws.com/doc/2006-03-01/" }, DynamicXmlBuilder.Fragment(list => + { + list.Owner(DynamicXmlBuilder.Fragment(owner => + { + owner.ID("id"); + owner.DisplayName("name"); + })); + + list.Buckets(DynamicXmlBuilder.Fragment(buckets => + { + foreach (var bucketItem in bucketList) + { + var item = bucketItem; + buckets.Bucket(DynamicXmlBuilder.Fragment(bucket => + { + bucket.Name(item.Id); + bucket.CreationDate(item.CreationDate.ToString("o")); + })); + } + })); + })); + + var result = builder.ToString(false); + return result; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/Serializers/BucketNotFoundSerializer.cs b/src/S3Emulator/Server/Responses/Serializers/BucketNotFoundSerializer.cs new file mode 100644 index 0000000..307f501 --- /dev/null +++ b/src/S3Emulator/Server/Responses/Serializers/BucketNotFoundSerializer.cs @@ -0,0 +1,22 @@ +namespace S3Emulator.Server.Responses.Serializers +{ + public class BucketNotFoundSerializer : AbstractS3Serializer + { + protected override string SerializeInternal(BucketNotFound bucketNotFound) + { + dynamic builder = new DynamicXmlBuilder(); + builder.Declaration(); + builder.Error(DynamicXmlBuilder.Fragment(error => + { + error.Code("NoSuchBucket"); + error.Message("The specified bucket does not exist"); + error.Resource(bucketNotFound.BucketName); + error.RequestId(1); + error.HostId(1); + })); + + var responseBody = builder.ToString(false); + return responseBody; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/Serializers/IS3Serializer.cs b/src/S3Emulator/Server/Responses/Serializers/IS3Serializer.cs new file mode 100644 index 0000000..3ac5275 --- /dev/null +++ b/src/S3Emulator/Server/Responses/Serializers/IS3Serializer.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace S3Emulator.Server.Responses.Serializers +{ + public interface IS3Serializer + { + void Serialize(TModel model, Stream stream); + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/Serializers/NullSerializer.cs b/src/S3Emulator/Server/Responses/Serializers/NullSerializer.cs new file mode 100644 index 0000000..6a6b878 --- /dev/null +++ b/src/S3Emulator/Server/Responses/Serializers/NullSerializer.cs @@ -0,0 +1,11 @@ +using System.IO; + +namespace S3Emulator.Server.Responses.Serializers +{ + public class NullSerializer : IS3Serializer + { + public void Serialize(TModel model, Stream stream) + { + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/Responses/Serializers/S3ObjectSearchResponeSerializer.cs b/src/S3Emulator/Server/Responses/Serializers/S3ObjectSearchResponeSerializer.cs new file mode 100644 index 0000000..1ae7b33 --- /dev/null +++ b/src/S3Emulator/Server/Responses/Serializers/S3ObjectSearchResponeSerializer.cs @@ -0,0 +1,40 @@ +using System.Xml; + +namespace S3Emulator.Server.Responses.Serializers +{ + public class S3ObjectSearchResponeSerializer : AbstractS3Serializer + { + protected override string SerializeInternal(S3ObjectSearchResponse searchResponse) + { + dynamic builder = new DynamicXmlBuilder(); + builder.Declaration(); + builder.ListBucketResult(new { xmlns = "http://s3.amazonaws.com/doc/2006-03-01/" }, DynamicXmlBuilder.Fragment(list => + { + list.Name(searchResponse.BucketName); + list.Prefix(searchResponse.Prefix); + list.Marker(searchResponse.Marker); + list.MaxKeys(searchResponse.MaxKeys); + list.IsTruncated(XmlConvert.ToString(searchResponse.IsTruncated)); + list.Contents(DynamicXmlBuilder.Fragment(contents => + { + foreach (var s3Object in searchResponse.S3Objects) + { + contents.Key(s3Object.Key); + contents.LastModifed(s3Object.CreationDate.ToString("o")); + contents.ETag(string.Format("\"{0}\"", s3Object.ContentMD5)); + contents.Size(s3Object.Size); + contents.StorageClass("STANDARD"); + contents.Owner(DynamicXmlBuilder.Fragment(owner => + { + owner.ID("id"); + owner.DisplayName("name"); + })); + } + })); + })); + + var responseBody = builder.ToString(false); + return responseBody; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Server/S3Server.cs b/src/S3Emulator/Server/S3Server.cs new file mode 100644 index 0000000..1355fc3 --- /dev/null +++ b/src/S3Emulator/Server/S3Server.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using Fiddler; +using Nancy.Hosting.Self; +using S3Emulator.Config; + +namespace S3Emulator.Server +{ + public class S3Server : IDisposable + { + private readonly S3Configuration s3Configuration; + private NancyHost nancyHost; + private Bootstrapper bootstrapper; + + public S3Server(S3Configuration s3Configuration) + { + this.s3Configuration = s3Configuration; + } + + public void Start() + { + if (s3Configuration.IsProxyEnabled) + { + FiddlerApplication.BeforeRequest += VirtualHostedToPathStyleBucketName; + FiddlerApplication.Startup(s3Configuration.ProxyPort, FiddlerCoreStartupFlags.Default); + } + + var uri = new Uri(string.Format("http://{0}:{1}", s3Configuration.Host, s3Configuration.HostPort)); + bootstrapper = new Bootstrapper(s3Configuration); + nancyHost = new NancyHost(bootstrapper, uri); + nancyHost.Start(); + } + + private void VirtualHostedToPathStyleBucketName(Session session) + { + if (!session.hostname.EndsWith(s3Configuration.ServiceUrl)) + { + return; + } + + string bucket = string.Empty; + if (!session.HostnameIs(s3Configuration.ServiceUrl)) + { + string virtualHostedPath = session.hostname.Replace("." + s3Configuration.ServiceUrl, string.Empty); + bucket = "/" + virtualHostedPath; + } + + session.host = string.Format("{0}:{1}", s3Configuration.Host, s3Configuration.HostPort); + session.PathAndQuery = string.Format("{0}{1}", bucket, session.PathAndQuery); + } + + public void Dispose() + { + if (s3Configuration.IsProxyEnabled) + { + FiddlerApplication.Shutdown(); + Thread.Sleep(750); + } + + bootstrapper.Dispose(); + nancyHost.Stop(); + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Storage/IS3Storage.cs b/src/S3Emulator/Storage/IS3Storage.cs new file mode 100644 index 0000000..4d472b4 --- /dev/null +++ b/src/S3Emulator/Storage/IS3Storage.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using S3Emulator.Model; +using S3Emulator.Server.Responses; + +namespace S3Emulator.Storage +{ + public interface IS3Storage + { + void AddBucket(Bucket bucket); + IEnumerable GetBuckets(); + void DeleteBucket(string bucket); + Bucket GetBucket(string bucket); + S3ObjectSearchResponse GetObjects(S3ObjectSearchRequest searchRequest); + void AddObject(S3Object s3Object); + void DeleteObject(string bucket, string key); + S3Object GetObject(string bucket, string key); + } +} \ No newline at end of file diff --git a/src/S3Emulator/Storage/Indexes/S3Object_Search.cs b/src/S3Emulator/Storage/Indexes/S3Object_Search.cs new file mode 100644 index 0000000..bf21bda --- /dev/null +++ b/src/S3Emulator/Storage/Indexes/S3Object_Search.cs @@ -0,0 +1,15 @@ +using System.Linq; +using Raven.Client.Indexes; +using S3Emulator.Model; + +namespace S3Emulator.Storage.Indexes +{ + public class S3Object_Search : AbstractIndexCreationTask + { + public S3Object_Search() + { + Map = s3Objects => from s3Object in s3Objects + select new { s3Object.Key }; + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/Storage/RavenDBStorage.cs b/src/S3Emulator/Storage/RavenDBStorage.cs new file mode 100644 index 0000000..922d247 --- /dev/null +++ b/src/S3Emulator/Storage/RavenDBStorage.cs @@ -0,0 +1,139 @@ +using System.Collections.Generic; +using System.Linq; +using Raven.Client; +using Raven.Json.Linq; +using S3Emulator.Model; +using S3Emulator.Server.Responses; +using S3Emulator.Storage.Indexes; + +namespace S3Emulator.Storage +{ + public class RavenDBStorage : IS3Storage + { + private readonly IDocumentStore documentStore; + + public RavenDBStorage(IDocumentStore documentStore) + { + this.documentStore = documentStore; + } + + public void AddBucket(Bucket bucket) + { + using (var session = documentStore.OpenSession()) + { + session.Store(bucket); + session.SaveChanges(); + } + } + + public Bucket GetBucket(string bucketName) + { + using (var session = documentStore.OpenSession()) + { + var bucket = session.Load(bucketName); + return bucket; + } + } + + public void DeleteBucket(string bucketName) + { + using (var session = documentStore.OpenSession()) + { + var bucket = session.Load(bucketName); + session.Delete(bucket); + session.SaveChanges(); + } + } + + public IEnumerable GetBuckets() + { + using (var session = documentStore.OpenSession()) + { + var buckets = session.Query() + .Take(1000) + .ToList(); + return buckets; + } + } + + public void AddObject(S3Object s3Object) + { + using (var session = documentStore.OpenSession()) + { + var content = s3Object.Content(); + content.Position = 0; + + session.Advanced.DatabaseCommands.PutAttachment(s3Object.Id, null, content, new RavenJObject()); + session.Store(s3Object); + session.SaveChanges(); + } + } + + public S3Object GetObject(string bucket, string key) + { + using (var session = documentStore.OpenSession()) + { + var s3Object = session.Load(bucket + "/" + key); + var attachment = session.Advanced.DatabaseCommands.GetAttachment(s3Object.Id); + s3Object.Content = attachment.Data; + + return s3Object; + } + } + + public void DeleteObject(string bucket, string key) + { + using (var session = documentStore.OpenSession()) + { + var s3Object = session.Load(bucket + "/" + key); + session.Advanced.DatabaseCommands.DeleteAttachment(s3Object.Id, null); + session.Delete(s3Object); + session.SaveChanges(); + } + } + + public S3ObjectSearchResponse GetObjects(S3ObjectSearchRequest searchRequest) + { + var searchResponse = new S3ObjectSearchResponse + { + BucketName = searchRequest.BucketName, + Delimiter = searchRequest.Delimiter, + Marker = searchRequest.Marker, + MaxKeys = searchRequest.MaxKeys, + Prefix = searchRequest.Prefix, + IsTruncated = false, + S3Objects = new List() + }; + + var bucket = GetBucket(searchRequest.BucketName); + if (bucket == null) + { + return searchResponse; + } + + var s3Objects = QueryS3Objects(searchRequest); + searchResponse.S3Objects = s3Objects; + + return searchResponse; + } + + private IEnumerable QueryS3Objects(S3ObjectSearchRequest searchRequest) + { + using (var session = documentStore.OpenSession()) + { + var query = session.Advanced.LuceneQuery(); + + if (!string.IsNullOrWhiteSpace(searchRequest.Prefix)) + { + var whereClause = string.Format("Key:{0}*", searchRequest.Prefix); + query = query.Where(whereClause); + } + + var result = query.Take(searchRequest.MaxKeys ?? 1000) + .WaitForNonStaleResultsAsOfNow() + .ToList(); + return result; + } + } + } +} \ No newline at end of file diff --git a/src/S3Emulator/app.config b/src/S3Emulator/app.config new file mode 100644 index 0000000..d6ec4b0 --- /dev/null +++ b/src/S3Emulator/app.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/S3Emulator/favicon.ico b/src/S3Emulator/favicon.ico new file mode 100644 index 0000000..0782874 Binary files /dev/null and b/src/S3Emulator/favicon.ico differ diff --git a/src/S3Emulator/packages.config b/src/S3Emulator/packages.config new file mode 100644 index 0000000..e3950b4 --- /dev/null +++ b/src/S3Emulator/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/S3Emulator.Tests/IO/ThrottledStreamTests.cs b/test/S3Emulator.Tests/IO/ThrottledStreamTests.cs new file mode 100644 index 0000000..29bf423 --- /dev/null +++ b/test/S3Emulator.Tests/IO/ThrottledStreamTests.cs @@ -0,0 +1,113 @@ +using System; +using System.IO; +using S3Emulator.IO; +using Xunit; + +namespace S3Emulator.Tests.IO +{ + public class ThrottledStreamTests + { + [Fact] + public void Should_Throttle_On_Read() + { + const int maximumBytesPerSecond = 1024; + const int bufferSize = 1024; + + Stream sourceStream = null; + Stream destinationStream = null; + + try + { + var bytes = new byte[4096]; + for (int i = 0; i < 4096; i++) + { + bytes[i] = 1; + } + + sourceStream = new ThrottledStream(new MemoryStream(bytes), maximumBytesPerSecond); + destinationStream = new MemoryStream(); + + var buffer = new byte[bufferSize]; + var start = Environment.TickCount; + int readCount = sourceStream.Read(buffer, 0, bufferSize); + AssertBytesPerSecond(start, readCount, maximumBytesPerSecond); + + while (readCount > 0) + { + start = Environment.TickCount; + destinationStream.Write(buffer, 0, readCount); + readCount = sourceStream.Read(buffer, 0, bufferSize); + AssertBytesPerSecond(start, readCount, maximumBytesPerSecond); + } + } + finally + { + if (destinationStream != null) + { + destinationStream.Close(); + } + + if (sourceStream != null) + { + sourceStream.Close(); + } + } + } + + [Fact] + public void Should_Throttle_On_Write() + { + const int maximumBytesPerSecond = 1024; + const int bufferSize = 1024; + + Stream sourceStream = null; + Stream destinationStream = null; + + try + { + var bytes = new byte[4096]; + for (int i = 0; i < 4096; i++) + { + bytes[i] = 1; + } + + sourceStream = new MemoryStream(bytes); + destinationStream = new ThrottledStream(new MemoryStream(), maximumBytesPerSecond); + + var buffer = new byte[bufferSize]; + int readCount = sourceStream.Read(buffer, 0, bufferSize); + + while (readCount > 0) + { + var start = Environment.TickCount; + destinationStream.Write(buffer, 0, readCount); + AssertBytesPerSecond(start, readCount, maximumBytesPerSecond); + + readCount = sourceStream.Read(buffer, 0, bufferSize); + } + } + finally + { + if (destinationStream != null) + { + destinationStream.Close(); + } + + if (sourceStream != null) + { + sourceStream.Close(); + } + } + } + + private void AssertBytesPerSecond(int start, long byteCount, long maxBps) + { + maxBps += 50; //give max bps some tolerance + var end = Environment.TickCount; + long elapsedMilliseconds = end - start; + if (elapsedMilliseconds == 0) elapsedMilliseconds = 1; + var bps = byteCount * 1000L / elapsedMilliseconds; + Assert.True(bps <= maxBps, string.Format("bps is {0}", bps)); + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Properties/AssemblyInfo.cs b/test/S3Emulator.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..0468701 --- /dev/null +++ b/test/S3Emulator.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("S3Emulator.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("S3Emulator.Tests")] +[assembly: AssemblyCopyright("Copyright © 2012")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3469fcee-e835-4fdc-a253-6c2c0ce858b7")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/test/S3Emulator.Tests/S3Emulator.Tests.csproj b/test/S3Emulator.Tests/S3Emulator.Tests.csproj new file mode 100644 index 0000000..2f8ea5d --- /dev/null +++ b/test/S3Emulator.Tests/S3Emulator.Tests.csproj @@ -0,0 +1,146 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {CFFD14BE-18CE-43D8-8CF6-05DBEBFF03AE} + Library + Properties + S3Emulator.Tests + S3Emulator.Tests + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\AsyncCtpLibrary.dll + + + False + ..\..\packages\AWSSDK.1.4.9.0\lib\AWSSDK.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\BouncyCastle.Crypto.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Esent.Interop.dll + + + ..\..\packages\Nancy.Testing.0.11.0\lib\net40\HtmlAgilityPack.dll + + + ..\..\packages\Nancy.Testing.0.11.0\lib\net40\HtmlAgilityPlus.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\ICSharpCode.NRefactory.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.Contrib.Spatial.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Lucene.Net.Contrib.SpellChecker.dll + + + ..\..\packages\Moq.4.0.10827\lib\NET40\Moq.dll + + + False + ..\..\packages\Nancy.0.11.0\lib\net40\Nancy.dll + + + False + ..\..\packages\Nancy.Testing.0.11.0\lib\net40\Nancy.Testing.dll + + + ..\..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll + + + ..\..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Abstractions.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.Embedded.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.Lightweight.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Client.MvcIntegration.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Database.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Munin.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Storage.Esent.dll + + + ..\..\packages\RavenDB-Embedded.1.0.888\lib\net40\Raven.Storage.Managed.dll + + + + + + + + + + + ..\..\packages\xunit.1.9.0.1566\lib\xunit.dll + + + + + + + + + + + + + + + + {BB9957DE-CD2E-4E76-A1E0-F251A2E91030} + S3Emulator + + + + + + + + \ No newline at end of file diff --git a/test/S3Emulator.Tests/Server/Modules/AbstractModuleTests.cs b/test/S3Emulator.Tests/Server/Modules/AbstractModuleTests.cs new file mode 100644 index 0000000..083b6cf --- /dev/null +++ b/test/S3Emulator.Tests/Server/Modules/AbstractModuleTests.cs @@ -0,0 +1,30 @@ +using Moq; +using Nancy; +using Nancy.Testing; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; + +namespace S3Emulator.Tests.Server.Modules +{ + public abstract class AbstractModuleTests where TModule : NancyModule + { + protected readonly Mock mockStorage; + protected readonly Mock mockResponder; + protected readonly Browser browser; + + protected AbstractModuleTests() + { + mockStorage = new Mock(); + mockResponder = new Mock(); + + var bootstrapper = new ConfigurableBootstrapper(x => + { + x.Dependency(mockStorage.Object); + x.Dependency(mockResponder.Object); + x.Module(); + }); + + browser = new Browser(bootstrapper); + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Server/Modules/BucketModuleTests.cs b/test/S3Emulator.Tests/Server/Modules/BucketModuleTests.cs new file mode 100644 index 0000000..41e00d0 --- /dev/null +++ b/test/S3Emulator.Tests/Server/Modules/BucketModuleTests.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using Moq; +using Nancy; +using Nancy.Testing; +using S3Emulator.Model; +using S3Emulator.Server.Modules; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; +using Xunit; + +namespace S3Emulator.Tests.Server.Modules +{ + public class BucketModuleTests : AbstractModuleTests + { + [Fact] + public void AddBucket_Should_Initialize_Name_And_CreationDate() + { + const string bucketName = "bucket1"; + Bucket bucket = null; + + mockStorage.Setup(x => x.AddBucket(It.IsAny())) + .Callback(x => bucket = x) + .Verifiable(); + + browser.Put("/" + bucketName); + + Assert.Equal(bucketName, bucket.Id); + Assert.True(bucket.CreationDate > DateTime.UtcNow.AddMinutes(-1)); + + mockStorage.Verify(); + } + + [Fact] + public void AddBucket_Should_Return_StatusOK() + { + var response = browser.Put("/bucket1"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public void DeleteBucket_Should_Return_StatusNoContent() + { + const string bucketName = "bucket1"; + + mockStorage.Setup(x => x.DeleteBucket(bucketName)) + .Verifiable(); + + var response = browser.Delete("/" + bucketName); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + mockStorage.Verify(); + } + + [Fact] + public void HeadBucket_Should_Return_StatusOK_If_Bucket_Exist() + { + mockStorage.Setup(x => x.GetBucket("bucket1")) + .Returns(new Bucket()) + .Verifiable(); + + var response = browser.Head("/bucket1"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + mockStorage.Verify(); + } + + [Fact] + public void HeadBucket_Should_Return_StatusNotFound_If_Bucket_Not_Exist() + { + mockStorage.Setup(x => x.GetBucket("bucket1")) + .Verifiable(); + + mockResponder.Setup(x => x.Respond(It.IsAny())) + .Returns(new Response()) + .Verifiable(); + + var response = browser.Head("/bucket1"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + mockStorage.Verify(); + mockResponder.Verify(); + } + + [Fact] + public void ListObjects_Should_Map_Query_Parameters() + { + S3ObjectSearchRequest searchRequest = null; + + mockStorage.Setup(x => x.GetBucket("bucket1")) + .Returns(new Bucket()) + .Verifiable(); + + mockStorage.Setup(x => x.GetObjects(It.IsAny())) + .Returns(new S3ObjectSearchResponse()) + .Callback(x => searchRequest = x) + .Verifiable(); + + mockResponder.Setup(x => x.Respond(It.IsAny())) + .Returns(new Response()) + .Verifiable(); + + browser.Get("/bucket1", x => + { + x.Query("prefix", "p"); + x.Query("delimiter", "d"); + x.Query("marker", "m"); + x.Query("max-keys", "10"); + }); + + Assert.Equal("p", searchRequest.Prefix); + Assert.Equal("d", searchRequest.Delimiter); + Assert.Equal("m", searchRequest.Marker); + Assert.Equal(10, searchRequest.MaxKeys); + + mockStorage.Verify(); + mockResponder.Verify(); + } + + [Fact] + public void ListObjects_Should_Return_StatusNotFound_If_Bucket_Does_Not_Exist() + { + mockStorage.Setup(x => x.GetBucket("bucket1")) + .Verifiable(); + + mockResponder.Setup(x => x.Respond(It.IsAny())) + .Returns(new Response()) + .Verifiable(); + + var response = browser.Get("/bucket1"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + mockStorage.Verify(); + mockResponder.Verify(); + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Server/Modules/S3ObjectModuleTests.cs b/test/S3Emulator.Tests/Server/Modules/S3ObjectModuleTests.cs new file mode 100644 index 0000000..8381425 --- /dev/null +++ b/test/S3Emulator.Tests/Server/Modules/S3ObjectModuleTests.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; +using System.Text; +using Moq; +using Nancy; +using Nancy.Testing; +using S3Emulator.Model; +using S3Emulator.Server.Modules; +using S3Emulator.Server.Responses; +using Xunit; + +namespace S3Emulator.Tests.Server.Modules +{ + public class S3ObjectModuleTests : AbstractModuleTests + { + [Fact] + public void AddObject_Should_Initialize_S3Object() + { + S3Object s3Object = null; + mockStorage.Setup(x => x.AddObject(It.IsAny())) + .Callback(x => s3Object = x) + .Verifiable(); + + var stream = new MemoryStream(Encoding.UTF8.GetBytes("foo")); + browser.Put("/bucket1/object1", x => x.Body(stream)); + + Assert.Equal("bucket1", s3Object.Bucket); + Assert.Equal("object1", s3Object.Key); + Assert.True(s3Object.CreationDate > DateTime.UtcNow.AddMinutes(-1)); + Assert.Equal(stream.Length, s3Object.Content().Length); + Assert.NotNull(s3Object.ContentMD5); + Assert.NotNull(s3Object.ContentType); + + mockStorage.Verify(); + } + + [Fact] + public void AddObject_Respone_Should_Include_ETag_Header() + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes("foo")); + var response = browser.Put("/bucket1/object1", x => x.Body(stream)); + Assert.NotNull(response.Headers["ETag"]); + } + + [Fact] + public void GetObject_Should_Return_StatusNotFound_If_Object_Not_Found() + { + mockStorage.Setup(x => x.GetObject("bucket1", "object1")) + .Verifiable(); + + var response = browser.Get("/bucket1/object1"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + mockStorage.Verify(); + } + + [Fact] + public void GetObject_Response_Should_Include_ETag_And_AcceptRanges_Headers() + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes("foo")); + + var s3Object = new S3Object + { + Content = () => stream, + ContentMD5 = "ContentMD5", + ContentType = "ContentType" + }; + + mockStorage.Setup(x => x.GetObject("bucket1", "object1")) + .Returns(s3Object).Verifiable(); + + var response = browser.Get("/bucket1/object1"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("ContentType", response.Context.Response.ContentType); + Assert.NotNull(response.Headers["ETag"]); + Assert.NotNull(response.Headers["Accept-Ranges"]); + Assert.Equal("foo", response.Body.AsString()); + } + + [Fact] + public void DeleteObject_Should_Return_StatusNoContent() + { + mockStorage.Setup(x => x.DeleteObject("bucket1", "object1")) + .Verifiable(); + + var response = browser.Delete("/bucket1/object1"); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + mockStorage.Verify(); + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Server/Modules/ServiceModuleTests.cs b/test/S3Emulator.Tests/Server/Modules/ServiceModuleTests.cs new file mode 100644 index 0000000..d15f963 --- /dev/null +++ b/test/S3Emulator.Tests/Server/Modules/ServiceModuleTests.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Moq; +using Nancy; +using S3Emulator.Model; +using S3Emulator.Server.Modules; +using Xunit; + +namespace S3Emulator.Tests.Server.Modules +{ + public class ServiceModuleTests : AbstractModuleTests + { + [Fact] + public void ListBuckets_Should_Return_StatusOK() + { + mockStorage.Setup(x => x.GetBuckets()) + .Returns(new List()) + .Verifiable(); + + mockResponder.Setup(x => x.Respond(It.IsAny>())) + .Returns(new Response { StatusCode = HttpStatusCode.OK }) + .Verifiable(); + + var response = browser.Get("/"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + mockStorage.Verify(); + mockResponder.Verify(); + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Server/Responses/DynamicXmlBuilderTests.cs b/test/S3Emulator.Tests/Server/Responses/DynamicXmlBuilderTests.cs new file mode 100644 index 0000000..5c57d99 --- /dev/null +++ b/test/S3Emulator.Tests/Server/Responses/DynamicXmlBuilderTests.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Xml.Linq; +using S3Emulator.Server.Responses; +using Xunit; + +namespace S3Emulator.Tests.Server.Responses +{ + public class DynamicXmlBuilderTests + { + [Fact] + public void Tag_Not_Writes_Blank_XmlNamespaces() + { + const string xmlns = "http://foo.com"; + + dynamic builder = new DynamicXmlBuilder(); + builder.Foo(new { xmlns = xmlns }, DynamicXmlBuilder.Fragment(x => + x.Bar("foobar") + )); + + var namespaces = FindNamespaces(builder); + + Assert.Equal(1, namespaces.Length); + Assert.Equal(xmlns, namespaces[0]); + } + + [Fact] + public void Tag_Adds_Nested_XmlNamespace_Properly() + { + const string xmlns = "http://foo.com"; + const string xmlns2 = "http://foobar.com"; + + dynamic builder = new DynamicXmlBuilder(); + builder.Foo(new { xmlns = xmlns }, DynamicXmlBuilder.Fragment(x => + x.Bar("foobar", new { xmlns = xmlns2 }) + )); + + var namespaces = FindNamespaces(builder); + + Assert.Equal(2, namespaces.Length); + Assert.Equal(xmlns, namespaces[0]); + Assert.Equal(xmlns2, namespaces[1]); + } + + private static XNamespace[] FindNamespaces(DynamicXmlBuilder builder) + { + XDocument xDocument = builder.ToXDocument(); + var namespaces = xDocument.Root + .DescendantsAndSelf() + .Select(x => x.Name.Namespace) + .Distinct() + .ToArray(); + return namespaces; + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Server/Responses/S3XmlResponderTests.cs b/test/S3Emulator.Tests/Server/Responses/S3XmlResponderTests.cs new file mode 100644 index 0000000..e134a6e --- /dev/null +++ b/test/S3Emulator.Tests/Server/Responses/S3XmlResponderTests.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using S3Emulator.Model; +using S3Emulator.Server.Responses; +using S3Emulator.Server.Responses.Serializers; +using Xunit; + +namespace S3Emulator.Tests.Server.Responses +{ + public class S3XmlResponderTests + { + [Fact] + public void GetSerializer_Should_Return_Proper_Serializer() + { + var s3XmlResponder = new MockS3XmlResponder(); + + var serializer = s3XmlResponder.PublicGetSerializer(new List()); + Assert.IsType(serializer); + + serializer = s3XmlResponder.PublicGetSerializer(new S3ObjectSearchResponse()); + Assert.IsType(serializer); + + serializer = s3XmlResponder.PublicGetSerializer(new BucketNotFound()); + Assert.IsType(serializer); + + serializer = s3XmlResponder.PublicGetSerializer(new object()); + Assert.IsType(serializer); + } + + class MockS3XmlResponder : S3XmlResponder + { + public IS3Serializer PublicGetSerializer(object o) + { + return GetSerializer(o); + } + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/Storage/RavenDBStorageTests.cs b/test/S3Emulator.Tests/Storage/RavenDBStorageTests.cs new file mode 100644 index 0000000..a9adcde --- /dev/null +++ b/test/S3Emulator.Tests/Storage/RavenDBStorageTests.cs @@ -0,0 +1,242 @@ +using System; +using System.IO; +using System.Linq; +using Raven.Client.Embedded; +using Raven.Json.Linq; +using S3Emulator.IO; +using S3Emulator.Model; +using S3Emulator.Server.Responses; +using S3Emulator.Storage; +using S3Emulator.Storage.Indexes; +using Xunit; + +namespace S3Emulator.Tests.Storage +{ + public class RavenDBStorageTests + { + [Fact] + public void Should_Add_Bucket() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + var storage = new RavenDBStorage(documentStore); + storage.AddBucket(new Bucket { Id = "bucket1", CreationDate = DateTime.UtcNow }); + + using (var documentSession = documentStore.OpenSession()) + { + var buckets = documentSession.Query().ToList(); + Assert.Equal(1, buckets.Count); + } + } + } + + [Fact] + public void Should_Get_Bucket_By_Key() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + using (var session = documentStore.OpenSession()) + { + session.Store(new Bucket { Id = "bucket1" }); + session.SaveChanges(); + } + + var storage = new RavenDBStorage(documentStore); + var bucket = storage.GetBucket("bucket1"); + Assert.NotNull(bucket); + Assert.Equal("bucket1", bucket.Id); + } + } + + [Fact] + public void Should_Delete_Bucket() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + using (var session = documentStore.OpenSession()) + { + session.Store(new Bucket { Id = "bucket1" }); + session.SaveChanges(); + } + + var storage = new RavenDBStorage(documentStore); + storage.DeleteBucket("bucket1"); + + using (var documentSession = documentStore.OpenSession()) + { + var bucket = documentSession.Load("bucket1"); + Assert.Null(bucket); + } + } + } + + [Fact] + public void Should_Get_Bucket_List() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + using (var session = documentStore.OpenSession()) + { + session.Store(new Bucket { Id = "bucket1" }); + session.SaveChanges(); + } + + var storage = new RavenDBStorage(documentStore); + var buckets = storage.GetBuckets(); + Assert.Equal(1, buckets.Count()); + } + } + + [Fact] + public void Should_Add_Content_As_Attachment() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + var storage = new RavenDBStorage(documentStore); + + var s3Object = new S3Object + { + Bucket = "bucket1", + Key = "key1", + ContentMD5 = "md5", + ContentType = "text", + Content = () => new MemoryStream(new byte[] { 1, 2, 3 }) + }; + + storage.AddObject(s3Object); + + using (var documentSession = documentStore.OpenSession()) + { + var s3Objects = documentSession.Query().ToList(); + Assert.Equal(1, s3Objects.Count); + Assert.Null(s3Objects[0].Content); + + var attachment = documentSession.Advanced.DatabaseCommands.GetAttachment(s3Object.Id); + Assert.NotNull(attachment); + Assert.Equal(s3Object.Content().GenerateMD5CheckSum(), attachment.Data().GenerateMD5CheckSum()); + } + } + } + + [Fact] + public void Should_Get_S3Object_By_Key() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + using (var session = documentStore.OpenSession()) + { + var s3Object = new S3Object + { + Bucket = "bucket1", + Key = "key1", + ContentMD5 = "md5", + ContentType = "text", + Content = () => new MemoryStream(new byte[] { 1, 2, 3 }) + }; + session.Advanced.DatabaseCommands.PutAttachment(s3Object.Id, null, s3Object.Content(), new RavenJObject()); + session.Store(s3Object); + session.SaveChanges(); + } + + var storage = new RavenDBStorage(documentStore); + var s3Object2 = storage.GetObject("bucket1", "key1"); + Assert.NotNull(s3Object2); + } + } + + [Fact] + public void Should_Delete_S3Object() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + using (var session = documentStore.OpenSession()) + { + var s3Object = new S3Object + { + Bucket = "bucket1", + Key = "key1", + ContentMD5 = "md5", + ContentType = "text", + Content = () => new MemoryStream(new byte[] { 1, 2, 3 }) + }; + session.Advanced.DatabaseCommands.PutAttachment(s3Object.Id, null, s3Object.Content(), new RavenJObject()); + session.Store(s3Object); + session.SaveChanges(); + } + + var storage = new RavenDBStorage(documentStore); + storage.DeleteObject("bucket1", "key1"); + + using (var documentSession = documentStore.OpenSession()) + { + var s3Object = documentSession.Load("bucket1/key1"); + Assert.Null(s3Object); + + var attachment = documentSession.Advanced.DatabaseCommands.GetAttachment("bucket1/key1"); + Assert.Null(attachment); + } + } + } + + [Fact] + public void Should_List_S3Objects_By_Prefix() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + Raven.Client.Indexes.IndexCreation.CreateIndexes(typeof(S3Object_Search).Assembly, documentStore); + + var storage = new RavenDBStorage(documentStore); + + storage.AddBucket(new Bucket { Id = "Bucket1", CreationDate = DateTime.UtcNow }); + + storage.AddObject(GetS3Object("Bucket1", "Photo/1.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/2.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/3.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/January/4.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/January/5.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/February/6.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/February/7.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/March/8.jpg")); + storage.AddObject(GetS3Object("Bucket1", "Photo/March/9.jpg")); + + var searchResponse = storage.GetObjects(new S3ObjectSearchRequest { BucketName = "Bucket1", Prefix = "Photo/January/" }); + Assert.Equal(2, searchResponse.S3Objects.Count()); + Assert.Equal("Photo/January/4.jpg", searchResponse.S3Objects.ElementAt(0).Key); + Assert.Equal("Photo/January/5.jpg", searchResponse.S3Objects.ElementAt(1).Key); + } + } + + [Fact] + public void Should_List_S3Objects_By_MaxKeys() + { + using (var documentStore = new EmbeddableDocumentStore { RunInMemory = true }.Initialize()) + { + Raven.Client.Indexes.IndexCreation.CreateIndexes(typeof(S3Object_Search).Assembly, documentStore); + + var storage = new RavenDBStorage(documentStore); + + storage.AddBucket(new Bucket { Id = "Bucket1", CreationDate = DateTime.UtcNow }); + + storage.AddObject(GetS3Object("Bucket1", "1.jpg")); + storage.AddObject(GetS3Object("Bucket1", "2.jpg")); + storage.AddObject(GetS3Object("Bucket1", "3.jpg")); + + var searchResponse = storage.GetObjects(new S3ObjectSearchRequest { BucketName = "Bucket1", MaxKeys = 2 }); + Assert.Equal(2, searchResponse.S3Objects.Count()); + Assert.Equal("1.jpg", searchResponse.S3Objects.ElementAt(0).Key); + Assert.Equal("2.jpg", searchResponse.S3Objects.ElementAt(1).Key); + } + } + + private static S3Object GetS3Object(string bucket, string key) + { + return new S3Object + { + Bucket = bucket, + Key = key, + CreationDate = DateTime.UtcNow, + Content = () => new MemoryStream(new byte[] { 1, 2, 3 }) + }; + } + } +} \ No newline at end of file diff --git a/test/S3Emulator.Tests/packages.config b/test/S3Emulator.Tests/packages.config new file mode 100644 index 0000000..01c92f2 --- /dev/null +++ b/test/S3Emulator.Tests/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/NuGet/NuGet.exe b/tools/NuGet/NuGet.exe new file mode 100644 index 0000000..34ad49b Binary files /dev/null and b/tools/NuGet/NuGet.exe differ