I am using Alamofire and try to get a JSON Feed from my Server. The Server has a self signed Certificate and Access via User and Password.
here's my Code
let user = "user"
let password = "password"
let url1 = "https://10.0.1.2:4711/fhem/?cmd=jsonlist2&XHR=1"
let credential = URLCredential(user: user, password: password, persistence: .forSession)
let serverTrustPolicies :[String: ServerTrustPolicy] = [
"10.0.1.8": .disableEvaluation
]
let AlamoSession = SessionManager(serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies))
AlamoSession.request(url1 )
.authenticate(usingCredential: credential)
.responseJSON { response in
debugPrint(response.response)
print("Result value \(response.result.value)")
//print (response.result.value?.valueForKey("status"))
}
but it does nor work!
How can i make it?
make a get Request on a https Url wir a self signed Certificate??
In the info.plist in my APP i add the
App Transport Security Settings - Allow Arbitrary Loads - YES
I ignored SSL errors using this code.
import Alamofire
struct WebAPI {
static let sessionManager: SessionManager = {
switch MyEnvironment.server {
case .development:
return Alamofire.SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManagerForDevelop()
)
case .staging, .production:
return Alamofire.SessionManager()
}
}()
private class ServerTrustPolicyManagerForDevelop: ServerTrustPolicyManager {
init() {
super.init(policies: [:])
}
override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
return .disableEvaluation
}
}
}
Related
I am following along with this tutorial in order to create an async generic network layer. I got the network manager working correctly.
https://betterprogramming.pub/async-await-generic-network-layer-with-swift-5-5-2bdd51224ea9
As I try to implement more APIs, that I can use with the networking layer, some of the APIs require different tokens, different content in the body, or header etc, that I have to get at runtime.
In the snippet of code below from the tutorial, I get that we are building up the Movie endpoint based on .self, and then return the specific values we need. But the issue is, some of the data in this, for example, the access token, has to be hard coded here. I am looking for a way, that I can 'inject' the accessToken, and then it will be created with this new token. Again, the reason for this, is that in other APIs, the access token might not always be known.
protocol Endpoint {
var scheme: String { get }
var host: String { get }
var version: String? { get }
var path: String { get }
var method: RequestMethod { get }
var queryItems: [String: String]? { get }
var header: [String: String]? { get }
var body: [String: String]? { get }
}
extension MoviesEndpoint: Endpoint {
var path: String {
switch self {
case .topRated:
return "/3/movie/top_rated"
case .movieDetail(let id):
return "/3/movie/\(id)"
}
}
var method: RequestMethod {
switch self {
case .topRated, .movieDetail:
return .get
}
}
var header: [String: String]? {
// Access Token to use in Bearer header
let accessToken = "insert your access token here -> https://www.themoviedb.org/settings/api"
switch self {
case .topRated, .movieDetail:
return [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8"
]
}
}
var body: [String: String]? {
switch self {
case .topRated, .movieDetail:
return nil
}
}
For an example, I tried converting the var body to a function, so I could do
func body(_ bodyDict: [String, String]?) -> [String:String]? {
switch self{
case .test:
return bodyDict
}
The idea of above, was that I changed it to a function, so I could pass in a dict, and then return that dict in the api call, but that did not work. The MoviesEnpoint adheres to the extension Endpoint, which then gives the compiler error 'Protocol Methods must not have bodies'.
Is there a way to dependency inject runtime parameters into this Extension/Protocol method?
Change the declaration of MoviesEndpoint so that it stores the access token:
struct MoviesEndpoint {
var accessToken: String
var detail: Detail
enum Detail {
case topRated
case movieDetail(id: Int)
}
}
You'll need to change all the switch self statements to switch detail.
However, I think the solution in the article (four protocols) is overwrought.
Instead of a pile of protocols, make one struct with a single function property:
struct MovieDatabaseClient {
var getRaw: (MovieEndpoint) async throws -> (Data, URLResponse)
}
Extend it with a generic method to handle the response parsing and decoding:
extension MovieDatabaseClient {
func get<T: Decodable>(
endpoint: MovieEndpoint,
as responseType: T.Type = T.self
) async throws -> T {
let (data, response) = try await getRaw(endpoint)
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
switch response.statusCode {
case 200...299:
break
case 401:
throw URLError(.userAuthenticationRequired)
default:
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(responseType, from: data)
}
}
Provide a “live“ implementation that actually sends network requests:
extension MovieDatabaseClient {
static func live(host: String, accessToken: String) -> Self {
return .init { endpoint in
let request = try liveURLRequest(
host: host,
accessToken: accessToken,
endpoint: endpoint
)
return try await URLSession.shared.data(for: request)
}
}
// Factored out in case you want to write unit tests for it:
static func liveURLRequest(
host: String,
accessToken: String,
endpoint: MovieEndpoint
) throws -> URLRequest {
var components = URLComponents()
components.scheme = "https"
components.host = host
components.path = endpoint.urlPath
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(accessToken)",
"Content-Type": "application/json;charset=utf-8",
]
return request
}
}
extension MovieEndpoint {
var urlPath: String {
switch self {
case .topRated: return "/3/movie/top_rated"
case .movieDetail(id: let id): return "/3/movie/\(id)"
}
}
}
To use it in your app:
// At app startup...
let myAccessToken = "loaded from UserDefaults or something"
let client = MovieDatabaseClient.live(
host: "api.themoviedb.org",
accessToken: myAccessToken
)
// Using it:
let topRated: TopRated = try await client.get(endpoint: .topRated)
let movieDetail: MovieDetail = try await client.get(endpoint: .movieDetail(id: 123))
For testing, you can create a mock client by providing a single closure that fakes the network request/response. Simple examples:
extension MovieDatabaseClient {
static func mockSuccess<T: Encodable>(_ body: T) -> Self {
return .init { _ in
let data = try JSONEncoder().encode(body)
let response = HTTPURLResponse(
url: URL(string: "test")!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: nil
)!
return (data, response)
}
}
static func mockFailure(_ error: Error) -> Self {
return .init { _ in
throw error
}
}
}
So a test can create a mock client that always responds with a TopRated response like this:
let mockTopRatedClient = MovieDatabaseClient.mockSuccess(TopRated(...))
If you want to learn more about this style of dependency management and mocking, Point-Free has a good (but subscription required) series of episodes: Designing Dependencies.
I'm trying to explore twitter API from VAPOR server. And I'm getting the following error:
[ INFO ] GET /hello [request-id: F00915F3-0812-429A-B012-BEDCAE1DD543]
[ WARNING ] read(descriptor:pointer:size:): Connection reset by peer (errno: 54) [request-id: F00915F3-0812-429A-B012-BEDCAE1DD543]
The problem is that I'm in Russia and you can get Twitter only through VPN. And it works just fine:
[ INFO ] GET /hello [request-id: 18EFDB9C-98CE-45F2-90E6-ABDC34983051]
Response(data: App.DataClass(id: "44196397", name: "Elon Musk", username: "elonmusk"))
GET request with 'Reqbin' or 'Postman' is okay. But when I'm deploying this project on 'render.com' or 'railway.com' which servers are outside the firewall, US and Germany, the problem remains the same.
So maybe something wrong with API? Or maybe I need some request tweaking.
I'm pretty sure this is not VAPOR problem, because the same error arises from XCode Project or Playground.
P.S. request from Windows Powershell the same situation, works with VPN. Maybe anyone has an idea in which direction to dig))
import Vapor
func routes(_ app: Application) throws {
app.get { req async in
"It works!"
}
app.get("hello") { req -> EventLoopFuture<Response> in
try getUser(request: req)
}
func getUser(request: Request) throws -> EventLoopFuture<Response> {
let headers: HTTPHeaders = HTTPHeaders([("Authorization", "Bearer \(Environment.get("BEARER_TOKEN")!)")])
let uri: URI = URI(string: "https://api.twitter.com/2/users/by/username/elonmusk")
return request.client.get(uri, headers: headers).flatMapThrowing { response in
guard response.status == .ok else { throw Abort(.unauthorized) }
guard let buffer = response.body else { throw Abort(.badRequest) }
guard let data = String(buffer: buffer).data(using: .utf8) else { throw Abort(.badRequest) }
do {
let data = try JSONDecoder().decode(Response.self, from: data)
print(data)
return data
} catch {
throw Abort(.badRequest)
}
}
}
}
// Response Model
struct Response: Content, Codable {
let data: DataClass
}
struct DataClass: Codable {
let id, name, username: String
}
I'm developing a MVVM structure with API calls.
I have this structure now:
//Get publisher
loginPublisher = LoginService.generateLoginPublisher()
//Create a subscriber
loginSubscriber = loginPublisher!
.sink { error in
print("Something bad happened")
self.isLoading = false
} receiveValue: { value in
self.saveClient(value)
self.client = value
self.isLoading = false
}
//Asking service to start assync task and notify its result on publisher
LoginService.login(email, password, loginPublisher!)
Basically what I do is obtain certain publisher from a LoginService, then I create a subscriber on loginPublisher, and then I tell LoginService to make some assync logic and send it result to loginPublisher this way I manage sent data with loginSubscriber.
I would like to execute LoginService.login() internally when I execute LoginService.generateLoginPublisher(), but if I do that, there is a chance that LoginService.login() logic finish before I create loginSubscriber, that's why I was forced to control when to call LoginService.login().
How could I detect from LoginService when its publisher has a new subscriber?
This is my LoginService class:
class LoginService{
static func generateLoginPublisher() -> PassthroughSubject<Client, NetworkError>{
return PassthroughSubject<Client, NetworkError>()
}
static func login(_ email: String,_ password: String,_ loginPublisher: PassthroughSubject<Client, NetworkError>){
let url = NetworkBuilder.getApiUrlWith(extraPath: "login")
print(url)
let parameters: [String: String] = [
"password": password,
"login": email
]
print(parameters)
let request = AF.request(
url, method: .post,
parameters: parameters,
encoder: JSONParameterEncoder.default
)
request.validate(statusCode: 200...299)
request.responseDecodable(of: Client.self) { response in
if let loginResponse = response.value{//Success
loginPublisher.send(loginResponse)
}
else{//Failure
loginPublisher.send(completion: Subscribers.Completion<NetworkError>.failure(.thingsJustHappen))
}
}
}
}
If you want full control over subscriptions, you can create a custom Publisher and Subscription.
Publisher's func receive<S: Subscriber>(subscriber: S) method is the one that gets called when the publisher receives a new subscriber.
If you simply want to make a network request when this happens, you just need to create a custom Publisher and return a Future that wraps the network request from this method.
In general, you should use Future for one-off async events, PassthroughSubject is not the ideal Publisher to use for network requests.
I finally solved my problem using Future instead of PassthroughtSubject as Dávid Pásztor suggested. Using Future I don't have to worried about LoginService.login() logic finish before I create loginSubscriber.
LoginSevice.login() method:
static func login(_ email: String,_ password: String) -> Future<Client, NetworkError>{
return Future<Client, NetworkError>{ completion in
let url = NetworkBuilder.getApiUrlWith(extraPath: "login")
print(url)
let parameters: [String: String] = [
"password": password,
"login": email
]
print(parameters)
let request = AF.request(
url, method: .post,
parameters: parameters,
encoder: JSONParameterEncoder.default
)
request.validate(statusCode: 200...299)
request.responseDecodable(of: Client.self) { response in
if let loginResponse = response.value{//Success
completion(.success(loginResponse))
}
else{//Failure
completion(.failure(NetworkError.thingsJustHappen))
}
}
}
}
Implementation:
loginSubscriber = LoginService.login(email, password)
.sink { error in
print("Something bad happened")
self.isLoading = false
} receiveValue: { value in
self.saveClient(value)
self.client = (value)
self.isLoading = false
}
You can handle publisher events and inject side effect to track subscriptions, like
class LoginService {
static func generateLoginPublisher(onSubscription: #escaping (Subscription) -> Void) -> AnyPublisher<Client, NetworkError> {
return PassthroughSubject<Client, NetworkError>()
.handleEvents(receiveSubscription: onSubscription, receiveOutput: nil, receiveCompletion: nil, receiveCancel: nil, receiveRequest: nil)
.eraseToAnyPublisher()
}
}
so
loginPublisher = LoginService.generateLoginPublisher() { subscription in
// do anything needed here when on new subscription appeared
}
Hy I'm trying to come up with a solution using Moya and RxSwift that renews an Authentication token and retries the requests.
The problem is I have multiple requests going on at the same time, so lets say 10 requests fire while the Authentication token has expired, I will try to renew the token on all of them, and as soon as the first one renews the other ones will fail because they use a wrong token to renew.
What I would like to do is just build a queue (maybe) of requests and then retry those. Not sure if this is the best scenario for this.
This is what I have so far:
final class NetworkOnlineProvider {
fileprivate let database = DatabaseClient(database: DatabaseRealm()).database
fileprivate let provider: MoyaProvider<NetworkAPI>
init(endpointClosure: #escaping MoyaProvider<NetworkAPI>.EndpointClosure = MoyaProvider<NetworkAPI>.defaultEndpointMapping,
requestClosure: #escaping MoyaProvider<NetworkAPI>.RequestClosure = MoyaProvider<NetworkAPI>.defaultRequestMapping,
stubClosure: #escaping MoyaProvider<NetworkAPI>.StubClosure = MoyaProvider.neverStub,
manager: Manager = MoyaProvider<NetworkAPI>.defaultAlamofireManager(),
plugins: [PluginType] = [],
trackInflights: Bool = false) {
self.provider = MoyaProvider(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, manager: manager, plugins: plugins, trackInflights: trackInflights)
}
fileprivate func getJWTRenewRequest() -> Single<Response>? {
if let token = JWTManager.sharedInstance.token {
return provider.rx.request(.renew(token: token))
}
return nil
}
func tokenRequest() -> Single<String> {
let errorSingle = Single<String>.create { single in
single(.error(APIError.failure))
return Disposables.create()
}
let emptyJWTSingle = Single<String>.create { single in
single(.success(""))
return Disposables.create()
}
// Return if no token found
guard let appToken = JWTManager.sharedInstance.getJWT() else {
return refreshToken() ?? emptyJWTSingle
}
// If we have a valid token, just return it
if !appToken.hasTokenExpired {
return Single<String>.create { single in
single(.success(appToken.token))
return Disposables.create()
}
}
// Token has expired
let newTokenRequest = refreshToken()
return newTokenRequest ?? errorSingle
}
func refreshToken() -> Single<String>? {
return getJWTRenewRequest()?
.debug("Renewing JWT")
.filterSuccessfulStatusCodes()
.map { (response: Response) -> (token: String, expiration: Double) in
guard let json = try? JSON(data: response.data) else { throw RxError.unknown }
let success = json["success"]
guard
let jwt = success["jwt"].string,
let jwt_expiration = success["jwt_expiration"].double,
let valid_login = success["valid_login"].bool, valid_login
else { throw RxError.unknown }
return (token: jwt, expiration: jwt_expiration)
}
.do(onSuccess: { (token: String, expiration: Double) in
JWTManager.sharedInstance.save(token: JWT(token: token, expiration: String(expiration)))
})
.map { (token: String, expiration: Double) in
return token
}
.catchError { e -> Single<String> in
print("Failed to Renew JWT")
JWTManager.sharedInstance.delete()
UIApplication.shared.appDelegate.cleanPreviousContext(jwt: true)
let loginVC = UIStoryboard(storyboard: .login).instantiateViewController(vc: LoginViewController.self)
UIApplication.shared.appDelegate.window?.setRootViewController(UINavigationController(rootViewController: loginVC))
throw e
}
}
func request(_ target: NetworkAPI) -> Single<Response> {
let actualRequest = provider.rx.request(target)
if target.isAuthenticatedCall {
return tokenRequest().flatMap { _ in
actualRequest
}
}
return actualRequest
}
}
The solution is here: RxSwift and Retrying a Network Request Despite Having an Invalid Token
The key is to use flatMapFirst so you only make one request for the first 401 and ignore other 401s while that request is in flight.
The gist associated with the article includes unit tests proving it works.
I would like to post binary data through RxAlamofire, Alamofire or even without any library but after some days of research and tries, I'm not able to do it.
Here you can find the POSTMAN example of the request that I am trying to reproduce is:
Is a post method with the Authorization and Content-Type headers and the binary data attached.
I have tried to find some example or something related but I couldn't find a solution. I could just find multipart form data examples but with multipart form data the server doesn't work (is a external API)
If someone could guide me or show me some example code.
Here the code used for login as example and to show you something that I want to achieve:
public class APIClient: DataSource {
public static var shared: APIClient = APIClient()
private init(){}
public func login(email:String, password:String) -> Observable<LoginResponse> {
return RxAlamofire.requestJSON(APIRouter.login(email:email, password:password))
.subscribeOn(MainScheduler.asyncInstance)
.debug()
.mapObject(type: LoginResponse.self)
}
}
Here the LoginResponse object:
public struct LoginResponse: Mappable {
var tokenId: String?
var userId: String?
public init?(map: Map) {}
public mutating func mapping(map: Map) {
tokenId <- map["id"]
userId <- map["userId"]
}
}
And finally the APIRouter extending URLRequestConvertible:
enum APIRouter: URLRequestConvertible {
case login(email: String, password: String)
private var method: HTTPMethod {
switch self {
case .login:
return .post
}
}
private var path: String {
switch self {
case .login:
return "users/login"
}
}
private var parameters: Parameters? {
switch self {
case .login(let email, let password):
return [APIConstants.LoginParameterKey.email: email, APIConstants.LoginParameterKey.password: password]
}
}
private var query: [URLQueryItem]? {
var queryItems = [URLQueryItem]()
switch self {
case .login:
return nil
}
}
func asURLRequest() throws -> URLRequest {
var urlComponents = URLComponents(string: APIConstants.ProductionServer.baseURL)!
if let query = query {
urlComponents.queryItems = query
}
var urlRequest = URLRequest(url: urlComponents.url!.appendingPathComponent(path))
// HTTP Method
urlRequest.httpMethod = method.rawValue
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue)
urlRequest.addValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
if let parameters = parameters {
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
}
return urlRequest
}
}
Thank you in advance!
EDIT To convert into RxAlamofire
With the code below I could solve the problem and convert it into RxSwift but I would like to use RxAlamofire to obtain the same result:
public func upload(media: Data) -> Observable<ContentUri> {
let headers = [
"content-type": "image/png",
"authorization": "token header"
]
return Observable<ContentUri>.create({observer in
Alamofire.upload(media, to: "\(endPoint)/api/media/upload", headers: headers)
.validate()
.responseJSON { response in
print(response)
}
return Disposables.create();
})
}
Alamofire.upload() (which returns an UploadRequest) might do what you want:
let headers = [
"Content-Type":"image/jpeg",
"Authorization":"sometoken",
]
let yourData = ... // Data of your image you want to upload
let endPoint = ...
Alamofire.upload(yourData, to: "\(endPoint)/api/media/upload", headers: headers)
.validate(statusCode: 200..<300)
.responseJSON { response in
// handle response
}
This example does not include RxAlamofire - but I am pretty sure it has a similar upload function. I hope it helps!