diff --git a/Hammerspoon Tests/HShttp.m b/Hammerspoon Tests/HShttp.m new file mode 100644 index 0000000000..641ffea1d0 --- /dev/null +++ b/Hammerspoon Tests/HShttp.m @@ -0,0 +1,47 @@ +// +// HShttp.m +// Hammerspoon +// +// Created by Alex Chen on 08/21/2022. + +#import "HSTestCase.h" + +@interface HThttp : HSTestCase + +@end + +@implementation HThttp + +- (void)setUp { + [super setUpWithRequire:@"test_http"]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +// Http tests + +- (void)testHttpDoAsyncRequestWithCachePolicyParam { + RUN_TWO_PART_LUA_TEST_WITH_TIMEOUT(5) +} + +- (void)testHttpDoAsyncRequestWithRedirectParamButNoCachePolicyParam { + RUN_LUA_TEST() +} + +- (void)testHttpDoAsyncRequestWithNoEnableRedirectParam { + RUN_TWO_PART_LUA_TEST_WITH_TIMEOUT(5) +} + +- (void)testHttpDoAsyncRequestWithRedirection { + RUN_TWO_PART_LUA_TEST_WITH_TIMEOUT(5) +} + +- (void)testHttpDoAsyncRequestWithoutRedirection { + RUN_TWO_PART_LUA_TEST_WITH_TIMEOUT(5) +} + +@end diff --git a/Hammerspoon.xcodeproj/project.pbxproj b/Hammerspoon.xcodeproj/project.pbxproj index 143c3e0490..60d558fb75 100644 --- a/Hammerspoon.xcodeproj/project.pbxproj +++ b/Hammerspoon.xcodeproj/project.pbxproj @@ -554,6 +554,9 @@ 949CDC001990759B00906CCE /* ConsoleWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 949CDBFF1990759B00906CCE /* ConsoleWindow.xib */; }; 94A1E5481993AC5C003AEB26 /* MJFileUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 94A1E5471993AC5C003AEB26 /* MJFileUtils.m */; }; B8524F32201A896000844F3D /* libhid.m in Sources */ = {isa = PBXBuildFile; fileRef = B8524F25201A875600844F3D /* libhid.m */; }; + CE139CB028B24B6500743C90 /* HShttp.m in Sources */ = {isa = PBXBuildFile; fileRef = CE139CAF28B24B6500743C90 /* HShttp.m */; }; + CE139CB128B24DAC00743C90 /* http.lua in Resources */ = {isa = PBXBuildFile; fileRef = 4F4CB4421B73AFD2000EA9B6 /* http.lua */; }; + CE139CB328B2539500743C90 /* test_http.lua in Resources */ = {isa = PBXBuildFile; fileRef = CE139CB228B2539500743C90 /* test_http.lua */; }; D02F95291A00221C00E28BB2 /* HSrequire_all.m in Sources */ = {isa = PBXBuildFile; fileRef = D02F95281A00221C00E28BB2 /* HSrequire_all.m */; }; D077B5BF1A001B5300369B30 /* variables.m in Sources */ = {isa = PBXBuildFile; fileRef = D077B5BE1A001B5300369B30 /* variables.m */; }; D61F4A341DCFA6DB00AEE223 /* libsharing.m in Sources */ = {isa = PBXBuildFile; fileRef = D61F4A311DCFA6C600AEE223 /* libsharing.m */; }; @@ -2005,6 +2008,8 @@ B8524F24201A875600844F3D /* hid.lua */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = hid.lua; path = extensions/hid/hid.lua; sourceTree = ""; }; B8524F25201A875600844F3D /* libhid.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = libhid.m; path = extensions/hid/libhid.m; sourceTree = ""; }; B8524F31201A88CD00844F3D /* libhid.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libhid.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; + CE139CAF28B24B6500743C90 /* HShttp.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HShttp.m; sourceTree = ""; }; + CE139CB228B2539500743C90 /* test_http.lua */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = test_http.lua; path = extensions/http/test_http.lua; sourceTree = ""; }; D02F95241A00221C00E28BB2 /* Hammerspoon Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Hammerspoon Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D02F95271A00221C00E28BB2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D02F95281A00221C00E28BB2 /* HSrequire_all.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HSrequire_all.m; sourceTree = ""; }; @@ -3150,6 +3155,7 @@ 4F4CB4411B73AFC1000EA9B6 /* http */ = { isa = PBXGroup; children = ( + CE139CB228B2539500743C90 /* test_http.lua */, 4F4CB4421B73AFD2000EA9B6 /* http.lua */, 4F4CB4431B73AFD2000EA9B6 /* libhttp.m */, ); @@ -4199,6 +4205,7 @@ 4FDD8E7B1C85A05700085D7A /* lsunit.lua */, D02F95261A00221C00E28BB2 /* Supporting Files */, 4FDD8E7D1C85A06800085D7A /* testinit.lua */, + CE139CAF28B24B6500743C90 /* HShttp.m */, ); path = "Hammerspoon Tests"; sourceTree = ""; @@ -6976,6 +6983,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + CE139CB128B24DAC00743C90 /* http.lua in Resources */, 4F7BB9D426447C390077A926 /* test_hotkey.lua in Resources */, 2273893624860C0E003EBC68 /* test_websocket.lua in Resources */, 4FCBAFD923B1152F007BA1D0 /* test_math.lua in Resources */, @@ -6998,6 +7006,7 @@ 4FDD8E7C1C85A05700085D7A /* lsunit.lua in Resources */, 4FDD8E7E1C85A06800085D7A /* testinit.lua in Resources */, 4FDD8E8B1C8C8BD200085D7A /* test_coresetup.lua in Resources */, + CE139CB328B2539500743C90 /* test_http.lua in Resources */, 4FEE39DB1F0A69D600935F90 /* test_crash.lua in Resources */, 4CB349F31C7F74EE006F0DE0 /* test_applescript.lua in Resources */, 4FB74B3C2049AB8B00B08851 /* test_task.lua in Resources */, @@ -7739,6 +7748,7 @@ files = ( 4FFF9FB525D89B3B00D2997C /* HSmouse.m in Sources */, 4FDD8E8F1C8DED1C00085D7A /* HSappfinder.m in Sources */, + CE139CB028B24B6500743C90 /* HShttp.m in Sources */, 6AE181321D39C8F10097211C /* HSnoises.m in Sources */, 4FCC52CE1E1527EF007F93D0 /* HSbrightness.m in Sources */, 22A6626225FC087000AA329E /* HSserial.m in Sources */, diff --git a/extensions/http/libhttp.m b/extensions/http/libhttp.m index 4a8b017236..ae04b0988e 100644 --- a/extensions/http/libhttp.m +++ b/extensions/http/libhttp.m @@ -23,6 +23,7 @@ static id responseBodyToId(NSHTTPURLResponse *httpResponse, NSData *bodyData) { // Definition of the collection delegate to receive callbacks from NSUrlConnection @interface connectionDelegate : NSObject @property int fn; +@property bool enableRedirect; @property(nonatomic, retain) NSMutableData* receivedData; @property(nonatomic, retain) NSHTTPURLResponse* httpResponse; @property(nonatomic, retain) NSURLConnection* connection; @@ -96,6 +97,37 @@ - (void)connection:(NSURLConnection * __unused)connection didFailWithError:(NSEr _lua_stackguard_exit(skin.L); } +- (NSURLRequest *)connection:(NSURLConnection *)connection + willSendRequest:(NSURLRequest *)request + redirectResponse:(NSURLResponse *)response { + + if (self.fn == LUA_NOREF) { + return nil; + } + + if ([response isKindOfClass:[NSHTTPURLResponse class]] && self.enableRedirect == false) { + NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; + + LuaSkin *skin = [LuaSkin sharedWithState:NULL]; + lua_State *L = skin.L; + _lua_stackguard_entry(L); + + [skin pushLuaRef:refTable ref:self.fn]; + lua_pushinteger(L, (int)httpResponse.statusCode); + [skin pushNSObject:responseBodyToId(self.httpResponse, self.receivedData)]; + [skin pushNSObject:httpResponse.allHeaderFields]; + [skin protectedCallAndError:@"hs.http connectionDelefate:didFinishLoading during redirection" nargs:3 nresults:0]; + + remove_delegate(L, self); + _lua_stackguard_exit(L); + + [connection cancel]; + return nil; + } + + return request; +} + @end // If the user specified a request body, get it from stack, @@ -166,7 +198,7 @@ static void extractHeadersFromStack(lua_State* L, int index, NSMutableURLRequest } } -/// hs.http.doAsyncRequest(url, method, data, headers, callback, [cachePolicy]) +/// hs.http.doAsyncRequest(url, method, data, headers, callback, [cachePolicy], [enableRedirect]) /// Function /// Creates an HTTP request and executes it asynchronously /// @@ -180,6 +212,7 @@ static void extractHeadersFromStack(lua_State* L, int index, NSMutableURLRequest /// * body - A string containing the body of the response /// * headers - A table containing the HTTP headers of the response /// * cachePolicy - An optional string containing the cache policy ("protocolCachePolicy", "ignoreLocalCache", "ignoreLocalAndRemoteCache", "returnCacheOrLoad", "returnCacheDontLoad" or "reloadRevalidatingCache"). Defaults to `protocolCachePolicy`. +/// * enableRedirect - An optional boolean to indicate whether to redirect the http request. Defaults to true. /// /// Returns: /// * None @@ -187,15 +220,21 @@ static void extractHeadersFromStack(lua_State* L, int index, NSMutableURLRequest /// Notes: /// * If authentication is required in order to download the request, the required credentials must be specified as part of the URL (e.g. "http://user:password@host.com/"). If authentication fails, or credentials are missing, the connection will attempt to continue without credentials. /// * If the Content-Type response header begins `text/` then the response body return value is a UTF8 string. Any other content type passes the response body, unaltered, as a stream of bytes. +/// * If enableRedirect is given, cachePolicy parameter can't be omitted or set to nil. If enableRedirect is set to true, response body will be empty string. This seems the limitation of 'connection:willSendRequest:redirectResponse' method. static int http_doAsyncRequest(lua_State* L){ LuaSkin *skin = [LuaSkin sharedWithState:L]; - [skin checkArgs:LS_TSTRING, LS_TSTRING, LS_TSTRING|LS_TNIL, LS_TTABLE|LS_TNIL, LS_TFUNCTION, LS_TSTRING | LS_TOPTIONAL, LS_TBREAK]; + [skin checkArgs:LS_TSTRING, LS_TSTRING, LS_TSTRING|LS_TNIL, LS_TTABLE|LS_TNIL, LS_TFUNCTION, LS_TSTRING | LS_TOPTIONAL, LS_TBOOLEAN | LS_TOPTIONAL, LS_TBREAK]; NSString* cachePolicy = nil; if (lua_type(L, 6) == LUA_TSTRING) { cachePolicy = [skin toNSObjectAtIndex:6]; } + bool enableRedirect = true; + if (lua_type(L, 7) == LUA_TBOOLEAN) { + enableRedirect = lua_toboolean(L, 7); + } + NSMutableURLRequest* request = getRequestFromStack(L, cachePolicy); getBodyFromStack(L, 3, request); extractHeadersFromStack(L, 4, request); @@ -204,6 +243,8 @@ static int http_doAsyncRequest(lua_State* L){ lua_pushvalue(L, 5); connectionDelegate* delegate = [[connectionDelegate alloc] init]; + delegate.enableRedirect = enableRedirect; + delegate.receivedData = [[NSMutableData alloc] init]; delegate.fn = [skin luaRef:refTable]; diff --git a/extensions/http/test_http.lua b/extensions/http/test_http.lua new file mode 100644 index 0000000000..eb573cd0b1 --- /dev/null +++ b/extensions/http/test_http.lua @@ -0,0 +1,149 @@ +hs.http = require("hs.http") + +respCode = 0 +respBody = "" +respHeaders = {} + +callback = function(code, body, headers) + respCode = code + respBody = body + respHeaders = headers +end + +-- check error should happen if cachePolicy is set to nil and enableRedirect is given +-- check point: pcall returns false, and error message contains "incorrect type 'nil' for argument 6 (expected string)" +function testHttpDoAsyncRequestWithRedirectParamButNoCachePolicyParam() + local ok, errMsg = pcall(function() + hs.http.doAsyncRequest( + 'http://google.com', + 'GET', + nil, + { ['accept-language'] = 'en', ['user-agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36', Accept = '*/*' }, + callback, + nil, -- this will trigger exception + false + ) + end) + assertIsEqual(false, ok) + print(hs.inspect(errMsg)) + local startIdx, _ = string.find(errMsg, "incorrect type 'nil' for argument 6 %(expected string%)") + assertGreaterThanOrEqualTo(0, startIdx) + + return success() +end + +function testHttpDoAsyncRequestWithCachePolicyParamValues() + if (type(respCode) == "number" and type(respBody) == "string" and type(respHeaders) == "table") then + -- check return code + assertIsEqual(200, respCode) + assertGreaterThan(0, string.len(respBody)) + return success() + else + return "Waiting for success..." + end +end + +-- check request should be redirected if enableRedirect is not given and cachePolicy param is given +-- check point: response code == 200 +function testHttpDoAsyncRequestWithCachePolicyParam() + respCode = 0 + respBody = "" + respHeaders = {} + hs.http.doAsyncRequest( + 'http://google.com', + 'GET', + nil, + { ['accept-language'] = 'en', ['user-agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36', Accept = '*/*' }, + callback, + 'protocolCachePolicy' + ) + + return success() +end + +function testHttpDoAsyncRequestWithNoEnableRedirectParamValues() + if (type(respCode) == "number" and type(respBody) == "string" and type(respHeaders) == "table") then + -- check return code + assertIsEqual(200, respCode) + assertGreaterThan(0, string.len(respBody)) + return success() + else + return "Waiting for success..." + end +end + +-- check request should be redirected if enableRedirect param is not given. +-- check point: response code == 200 +function testHttpDoAsyncRequestWithNoEnableRedirectParam() + respCode = 0 + respBody = "" + respHeaders = {} + hs.http.doAsyncRequest( + 'http://google.com', + 'GET', + nil, + { ['accept-language'] = 'en', ['user-agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36', Accept = '*/*' }, + callback + ) + + return success() +end + +function testHttpDoAsyncRequestWithRedirectionValues() + if (type(respCode) == "number" and type(respBody) == "string" and type(respHeaders) == "table") then + -- check return code + assertIsEqual(200, respCode) + assertGreaterThan(0, string.len(respBody)) + return success() + else + return "Waiting for success..." + end +end + +-- check request should be redirected if enableRedirect is set to true +-- check point: response code == 200 +function testHttpDoAsyncRequestWithRedirection() + respCode = 0 + respBody = "" + respHeaders = {} + hs.http.doAsyncRequest( + 'http://google.com', + 'GET', + nil, + { ['accept-language'] = 'en', ['user-agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36', Accept = '*/*' }, + callback, + 'protocolCachePolicy', + true + ) + + return success() +end + +function testHttpDoAsyncRequestWithoutRedirectionValues() + if (type(respCode) == "number" and type(respBody) == "string" and type(respHeaders) == "table") then + -- check return code + assertIsEqual(301, respCode) + return success() + else + return "Waiting for success..." + end +end + +-- check request should not be redirected if enableRedirect is set to false +-- check point: response code == 301 +function testHttpDoAsyncRequestWithoutRedirection() + respCode = 0 + respBody = "" + respHeaders = {} + hs.http.doAsyncRequest( + 'http://google.com', + 'GET', + nil, + { ['accept-language'] = 'en', ['user-agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36', Accept = '*/*' }, + callback, + 'protocolCachePolicy', + false + ) + + return success() +end