*&---------------------------------------------------------------------* *& Include ZABAPGIT_HTTP *&---------------------------------------------------------------------* CLASS lcl_http_client DEFINITION FINAL. PUBLIC SECTION. METHODS: constructor IMPORTING ii_client TYPE REF TO if_http_client, close, send_receive_close IMPORTING iv_data TYPE xstring RETURNING VALUE(rv_data) TYPE xstring RAISING lcx_exception, get_cdata RETURNING VALUE(rv_value) TYPE string, check_http_200 RAISING lcx_exception, send_receive RAISING lcx_exception, set_headers IMPORTING iv_url TYPE string iv_service TYPE string RAISING lcx_exception. PROTECTED SECTION. DATA: mi_client TYPE REF TO if_http_client. ENDCLASS. CLASS lcl_http_client IMPLEMENTATION. METHOD constructor. mi_client = ii_client. ENDMETHOD. METHOD send_receive_close. * do not use set_cdata as it modifies the Content-Type header field mi_client->request->set_data( iv_data ). send_receive( ). check_http_200( ). rv_data = mi_client->response->get_data( ). mi_client->close( ). ENDMETHOD. METHOD get_cdata. rv_value = mi_client->response->get_cdata( ). ENDMETHOD. METHOD close. mi_client->close( ). ENDMETHOD. METHOD set_headers. DATA: lv_value TYPE string. mi_client->request->set_header_field( name = '~request_method' value = 'POST' ). lv_value = lcl_url=>path_name( iv_url ) && '/git-' && iv_service && '-pack'. mi_client->request->set_header_field( name = '~request_uri' value = lv_value ). lv_value = 'application/x-git-' && iv_service && '-pack-request'. "#EC NOTEXT mi_client->request->set_header_field( name = 'Content-Type' value = lv_value ). "#EC NOTEXT lv_value = 'application/x-git-' && iv_service && '-pack-result'. "#EC NOTEXT mi_client->request->set_header_field( name = 'Accept' value = lv_value ). "#EC NOTEXT ENDMETHOD. "set_headers METHOD send_receive. DATA lv_text TYPE string. mi_client->send( ). mi_client->receive( EXCEPTIONS http_communication_failure = 1 http_invalid_state = 2 http_processing_failed = 3 OTHERS = 4 ). IF sy-subrc <> 0. CASE sy-subrc. WHEN 1. " make sure: " a) SSL is setup properly in STRUST " b) no firewalls " check trace file in transaction SMICM lv_text = 'HTTP Communication Failure'. "#EC NOTEXT WHEN 2. lv_text = 'HTTP Invalid State'. "#EC NOTEXT WHEN 3. lv_text = 'HTTP Processing failed'. "#EC NOTEXT WHEN OTHERS. lv_text = 'Another error occured'. "#EC NOTEXT ENDCASE. lcx_exception=>raise( lv_text ). ENDIF. ENDMETHOD. "send_receive METHOD check_http_200. DATA: lv_code TYPE i, lv_text TYPE string. mi_client->response->get_status( IMPORTING code = lv_code ). CASE lv_code. WHEN 200. RETURN. WHEN 302. lcx_exception=>raise( 'HTTP redirect, check URL' ). WHEN 401. lcx_exception=>raise( 'HTTP 401, unauthorized' ). WHEN 403. lcx_exception=>raise( 'HTTP 403, forbidden' ). WHEN 404. lcx_exception=>raise( 'HTTP 404, not found' ). WHEN 415. lcx_exception=>raise( 'HTTP 415, unsupported media type' ). WHEN OTHERS. lv_text = mi_client->response->get_cdata( ). lcx_exception=>raise( |HTTP error code: { lv_code }, { lv_text }| ). ENDCASE. ENDMETHOD. "http_200 ENDCLASS. CLASS lcl_http_digest DEFINITION FINAL. PUBLIC SECTION. CLASS-METHODS: run IMPORTING ii_client TYPE REF TO if_http_client iv_username TYPE string iv_password TYPE string. PRIVATE SECTION. CLASS-DATA: gv_nc TYPE n LENGTH 8. CLASS-METHODS: parse IMPORTING iv_value TYPE string EXPORTING ev_scheme TYPE string ev_realm TYPE string ev_qop TYPE string ev_nonce TYPE string, md5 IMPORTING iv_data TYPE string RETURNING VALUE(rv_hash) TYPE string, hash IMPORTING iv_qop TYPE string iv_realm TYPE string iv_nonce TYPE string iv_username TYPE clike iv_uri TYPE string iv_method TYPE string iv_cnonse TYPE string iv_password TYPE clike RETURNING VALUE(rv_response) TYPE string. ENDCLASS. CLASS lcl_http_digest IMPLEMENTATION. METHOD hash. DATA(lv_ha1) = md5( |{ iv_username }:{ iv_realm }:{ iv_password }| ). DATA(lv_ha2) = md5( |{ iv_method }:{ iv_uri }| ). rv_response = md5( |{ lv_ha1 }:{ iv_nonce }:{ gv_nc }:{ iv_cnonse }:{ iv_qop }:{ lv_ha2 }| ). ENDMETHOD. METHOD run. DATA: lv_value TYPE string, lv_scheme TYPE string, lv_realm TYPE string, lv_response TYPE string, lv_method TYPE string, lv_uri TYPE string, lv_auth TYPE string, lv_cnonce TYPE string, lv_qop TYPE string, lv_nonce TYPE string. lv_value = ii_client->response->get_header_field( 'www-authenticate' ). parse( EXPORTING iv_value = lv_value IMPORTING ev_scheme = lv_scheme ev_realm = lv_realm ev_qop = lv_qop ev_nonce = lv_nonce ). ASSERT NOT lv_nonce IS INITIAL. lv_method = 'GET'. lv_uri = ii_client->request->get_header_field( '~request_uri' ). CALL FUNCTION 'GENERAL_GET_RANDOM_STRING' EXPORTING number_chars = 24 IMPORTING random_string = lv_cnonce. lv_response = hash( iv_qop = lv_qop iv_realm = lv_realm iv_nonce = lv_nonce iv_username = iv_username iv_uri = lv_uri iv_method = lv_method iv_cnonse = lv_cnonce iv_password = iv_password ). * client response lv_auth = |Digest username="{ iv_username }", realm="{ lv_realm }", nonce="{ lv_nonce }", uri="{ lv_uri }", qop={ lv_qop }, nc={ gv_nc }, cnonce="{ lv_cnonce }", response="{ lv_response }"|. ii_client->request->set_header_field( name = 'Authorization' value = lv_auth ). ENDMETHOD. METHOD parse. CLEAR: ev_scheme, ev_realm, ev_qop, ev_nonce. FIND REGEX '^(\w+)' IN iv_value SUBMATCHES ev_scheme. FIND REGEX 'realm="([\w ]+)"' IN iv_value SUBMATCHES ev_realm. FIND REGEX 'qop="(\w+)"' IN iv_value SUBMATCHES ev_qop. FIND REGEX 'nonce="([\w=/+\$]+)"' IN iv_value SUBMATCHES ev_nonce. ENDMETHOD. METHOD md5. DATA: lv_xstr TYPE xstring, lv_hash TYPE xstring. lv_xstr = lcl_convert=>string_to_xstring_utf8( iv_data ). CALL FUNCTION 'CALCULATE_HASH_FOR_RAW' EXPORTING alg = 'MD5' data = lv_xstr IMPORTING hashxstring = lv_hash EXCEPTIONS unknown_alg = 1 param_error = 2 internal_error = 3 OTHERS = 4. IF sy-subrc <> 0. BREAK-POINT. ENDIF. rv_hash = lv_hash. TRANSLATE rv_hash TO LOWER CASE. ENDMETHOD. ENDCLASS. CLASS lcl_http DEFINITION FINAL. PUBLIC SECTION. CLASS-METHODS: get_agent RETURNING VALUE(rv_agent) TYPE string, create_by_url IMPORTING iv_url TYPE string iv_service TYPE string RETURNING VALUE(ro_client) TYPE REF TO lcl_http_client RAISING lcx_exception. PRIVATE SECTION. CLASS-METHODS: check_auth_requested IMPORTING ii_client TYPE REF TO if_http_client RETURNING VALUE(rv_auth_requested) TYPE abap_bool RAISING lcx_exception, is_local_system IMPORTING iv_url TYPE string RETURNING VALUE(rv_bool) TYPE abap_bool, acquire_login_details IMPORTING ii_client TYPE REF TO if_http_client iv_url TYPE string RAISING lcx_exception. ENDCLASS. CLASS lcl_http IMPLEMENTATION. METHOD get_agent. * bitbucket require agent prefix = "git/" rv_agent = 'git/abapGit-' && gc_abap_version. ENDMETHOD. METHOD create_by_url. DATA: lv_uri TYPE string, lv_expect_potentual_auth TYPE abap_bool, li_client TYPE REF TO if_http_client, lo_settings TYPE REF TO lcl_settings. lo_settings = lcl_app=>settings( )->read( ). cl_http_client=>create_by_url( EXPORTING url = lcl_url=>host( iv_url ) ssl_id = 'ANONYM' proxy_host = lo_settings->get_proxy_url( ) proxy_service = lo_settings->get_proxy_port( ) IMPORTING client = li_client ). CREATE OBJECT ro_client EXPORTING ii_client = li_client. IF is_local_system( iv_url ) = abap_true. li_client->send_sap_logon_ticket( ). ENDIF. li_client->request->set_cdata( '' ). li_client->request->set_header_field( name = '~request_method' value = 'GET' ). li_client->request->set_header_field( name = 'user-agent' value = get_agent( ) ). "#EC NOTEXT lv_uri = lcl_url=>path_name( iv_url ) && '/info/refs?service=git-' && iv_service && '-pack'. li_client->request->set_header_field( name = '~request_uri' value = lv_uri ). " Disable internal auth dialog (due to its unclarity) li_client->propertytype_logon_popup = if_http_client=>co_disabled. lcl_login_manager=>load( iv_uri = iv_url ii_client = li_client ). ro_client->send_receive( ). IF check_auth_requested( li_client ) = abap_true. acquire_login_details( ii_client = li_client iv_url = iv_url ). ro_client->send_receive( ). ENDIF. ro_client->check_http_200( ). lcl_login_manager=>save( iv_uri = iv_url ii_client = li_client ). ENDMETHOD. METHOD is_local_system. DATA: lv_host TYPE string, lt_list TYPE STANDARD TABLE OF icm_sinfo2 WITH DEFAULT KEY. CALL FUNCTION 'ICM_GET_INFO2' TABLES servlist = lt_list EXCEPTIONS icm_error = 1 icm_timeout = 2 icm_not_authorized = 3 OTHERS = 4. IF sy-subrc <> 0. RETURN. ENDIF. FIND REGEX 'https?://([^/^:]*)' IN iv_url SUBMATCHES lv_host. READ TABLE lt_list WITH KEY hostname = lv_host TRANSPORTING NO FIELDS. rv_bool = boolc( sy-subrc = 0 ). ENDMETHOD. METHOD check_auth_requested. DATA: lv_code TYPE i. ii_client->response->get_status( IMPORTING code = lv_code ). IF lv_code = 401. rv_auth_requested = abap_true. ENDIF. ENDMETHOD. "check_auth_requested METHOD acquire_login_details. DATA: lv_default_user TYPE string, lv_scheme TYPE string, lv_user TYPE string, lv_pass TYPE string. lv_default_user = lcl_app=>user( )->get_repo_username( iv_url ). lv_user = lv_default_user. lcl_password_dialog=>popup( EXPORTING iv_repo_url = iv_url CHANGING cv_user = lv_user cv_pass = lv_pass ). IF lv_user IS INITIAL. lcx_exception=>raise( 'HTTP 401, unauthorized' ). ENDIF. IF lv_user <> lv_default_user. lcl_app=>user( )->set_repo_username( iv_url = iv_url iv_username = lv_user ). ENDIF. lv_scheme = ii_client->response->get_header_field( 'www-authenticate' ). FIND REGEX '^(\w+)' IN lv_scheme SUBMATCHES lv_scheme. CASE lv_scheme. WHEN 'Digest'. * https://en.wikipedia.org/wiki/Digest_access_authentication * eg used by https://www.gerritcodereview.com/ lcl_http_digest=>run( ii_client = ii_client iv_username = lv_user iv_password = lv_pass ). WHEN OTHERS. * https://en.wikipedia.org/wiki/Basic_access_authentication ii_client->authenticate( username = lv_user password = lv_pass ). ENDCASE. ENDMETHOD. "acquire_login_details ENDCLASS.