Jelajahi Sumber

Use kang-core

世界 4 tahun lalu
induk
melakukan
d12b1a284e

+ 1 - 1
.gitmodules

@@ -48,7 +48,7 @@
 	url = https://github.com/SagerNet/preferencex-android
 [submodule "external/v2ray-core"]
 	path = external/v2ray-core
-	url = https://github.com/v2fly/v2ray-core
+	url = https://github.com/SagerNet/v2ray-core
 [submodule "plugin/trojan/src/main/cpp/igniter-libs"]
 	path = plugin/trojan/src/main/cpp/igniter-libs
 	url = https://github.com/trojan-gfw/igniter-libs

+ 1 - 0
.idea/dictionaries/sekai.xml

@@ -36,6 +36,7 @@
       <w>tproxy</w>
       <w>transproxy</w>
       <w>uplink</w>
+      <w>utls</w>
       <w>vless</w>
       <w>vmess</w>
       <w>websocket</w>

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/Constants.kt

@@ -96,6 +96,8 @@ object Key {
     const val PROVIDER_TROJAN = "providerTrojan"
     const val PROVIDER_SS_AEAD = "providerShadowsocksAEAD"
 
+    const val UTLS_FINGERPRINT = "utlsFingerprint"
+
     const val PROFILE_DIRTY = "profileDirty"
     const val PROFILE_ID = "profileId"
     const val PROFILE_NAME = "profileName"

+ 0 - 1
app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt

@@ -95,7 +95,6 @@ class SagerNet : Application(),
 
         val externalAssets = getExternalFilesDir(null) ?: filesDir
         Libcore.initializeV2Ray(externalAssets.absolutePath + "/", "v2ray/", true)
-        Libcore.setenv("v2ray.conf.geoloader", "memconservative")
 
         runOnDefaultDispatcher {
             externalAssets.mkdirs()

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt

@@ -198,6 +198,8 @@ object DataStore : OnPreferenceDataStoreChangeListener {
     var icmpEchoReplyDelay by configurationStore.stringToLong(Key.ICMP_ECHO_REPLY_DELAY) { 50 }
     var ipOtherStrategy by configurationStore.stringToInt(Key.IP_OTHER_STRATEGY) { PacketStrategy.DIRECT }
 
+    var utlsFingerprint by configurationStore.string(Key.UTLS_FINGERPRINT)
+
     // protocol
 
     var providerTrojan by configurationStore.stringToInt(Key.PROVIDER_TROJAN)

+ 119 - 120
app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt

@@ -182,8 +182,7 @@ fun buildV2RayConfig(
                     listen = LOCALHOST
                     port = uidPort
                     protocol = "socks"
-                    settings = LazyInboundConfigurationObject(
-                        this,
+                    settings = LazyInboundConfigurationObject(this,
                         SocksInboundConfigurationObject().apply {
                             auth = "noauth"
                             udp = true
@@ -267,8 +266,7 @@ fun buildV2RayConfig(
             listen = bind
             port = DataStore.socksPort
             protocol = "socks"
-            settings = LazyInboundConfigurationObject(
-                this,
+            settings = LazyInboundConfigurationObject(this,
                 SocksInboundConfigurationObject().apply {
                     auth = "noauth"
                     udp = true
@@ -293,8 +291,7 @@ fun buildV2RayConfig(
                 listen = bind
                 port = if (forTest) testPort else DataStore.httpPort
                 protocol = "http"
-                settings = LazyInboundConfigurationObject(
-                    this,
+                settings = LazyInboundConfigurationObject(this,
                     HTTPInboundConfigurationObject().apply {
                         allowTransparent = true
                         userLevel = 8
@@ -319,8 +316,7 @@ fun buildV2RayConfig(
                 listen = bind
                 port = DataStore.transproxyPort
                 protocol = "dokodemo-door"
-                settings = LazyInboundConfigurationObject(
-                    this,
+                settings = LazyInboundConfigurationObject(this,
                     DokodemoDoorInboundConfigurationObject().apply {
                         network = "tcp,udp"
                         followRedirect = true
@@ -401,6 +397,7 @@ fun buildV2RayConfig(
 
         var rootBalancer: RoutingObject.RuleObject? = null
 
+        val utlsFingerprint = DataStore.utlsFingerprint
         fun buildChain(
             tagOutbound: String,
             profileList: List<ProxyEntity>,
@@ -461,14 +458,14 @@ fun buildV2RayConfig(
                     chainMap[localPort] = proxyEntity
                     currentOutbound.apply {
                         protocol = "socks"
-                        settings = LazyOutboundConfigurationObject(
-                            this,
+                        settings = LazyOutboundConfigurationObject(this,
                             SocksOutboundConfigurationObject().apply {
-                                servers = listOf(SocksOutboundConfigurationObject.ServerObject()
-                                    .apply {
-                                        address = LOCALHOST
-                                        port = localPort
-                                    })
+                                servers = listOf(
+                                    SocksOutboundConfigurationObject.ServerObject()
+                                        .apply {
+                                            address = LOCALHOST
+                                            port = localPort
+                                        })
                             })
                     }
                 } else {
@@ -478,31 +475,34 @@ fun buildV2RayConfig(
 
                         if (bean is SOCKSBean) {
                             protocol = "socks"
-                            settings = LazyOutboundConfigurationObject(
-                                this,
+                            settings = LazyOutboundConfigurationObject(this,
                                 SocksOutboundConfigurationObject().apply {
-                                    servers = listOf(SocksOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            if (!bean.username.isNullOrBlank()) {
-                                                users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
-                                                    .apply {
-                                                        user = bean.username
-                                                        pass = bean.password
-                                                    })
-                                            }
-                                        })
+                                    servers = listOf(
+                                        SocksOutboundConfigurationObject.ServerObject()
+                                            .apply {
+                                                address = bean.serverAddress
+                                                port = bean.serverPort
+                                                if (!bean.username.isNullOrBlank()) {
+                                                    users = listOf(SocksOutboundConfigurationObject.ServerObject.UserObject()
+                                                        .apply {
+                                                            user = bean.username
+                                                            pass = bean.password
+                                                        })
+                                                }
+                                            })
                                 })
                             if (bean.tls || needKeepAliveInterval) {
                                 streamSettings = StreamSettingsObject().apply {
                                     network = "tcp"
                                     if (bean.tls) {
                                         security = "tls"
-                                        if (bean.sni.isNotBlank()) {
-                                            tlsSettings = TLSObject().apply {
+                                        tlsSettings = TLSObject().apply {
+                                            if (bean.sni.isNotBlank()) {
                                                 serverName = bean.sni
                                             }
+                                            if (utlsFingerprint.isNotBlank()) {
+                                                fingerprint = utlsFingerprint
+                                            }
                                         }
                                     }
                                     if (needKeepAliveInterval) {
@@ -514,31 +514,34 @@ fun buildV2RayConfig(
                             }
                         } else if (bean is HttpBean) {
                             protocol = "http"
-                            settings = LazyOutboundConfigurationObject(
-                                this,
+                            settings = LazyOutboundConfigurationObject(this,
                                 HTTPOutboundConfigurationObject().apply {
-                                    servers = listOf(HTTPOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            if (!bean.username.isNullOrBlank()) {
-                                                users = listOf(HTTPInboundConfigurationObject.AccountObject()
-                                                    .apply {
-                                                        user = bean.username
-                                                        pass = bean.password
-                                                    })
-                                            }
-                                        })
+                                    servers = listOf(
+                                        HTTPOutboundConfigurationObject.ServerObject()
+                                            .apply {
+                                                address = bean.serverAddress
+                                                port = bean.serverPort
+                                                if (!bean.username.isNullOrBlank()) {
+                                                    users = listOf(HTTPInboundConfigurationObject.AccountObject()
+                                                        .apply {
+                                                            user = bean.username
+                                                            pass = bean.password
+                                                        })
+                                                }
+                                            })
                                 })
                             if (bean.tls || needKeepAliveInterval) {
                                 streamSettings = StreamSettingsObject().apply {
                                     network = "tcp"
                                     if (bean.tls) {
                                         security = "tls"
-                                        if (bean.sni.isNotBlank()) {
-                                            tlsSettings = TLSObject().apply {
+                                        tlsSettings = TLSObject().apply {
+                                            if (bean.sni.isNotBlank()) {
                                                 serverName = bean.sni
                                             }
+                                            if (utlsFingerprint.isNotBlank()) {
+                                                fingerprint = utlsFingerprint
+                                            }
                                         }
                                     }
                                     if (needKeepAliveInterval) {
@@ -551,47 +554,47 @@ fun buildV2RayConfig(
                         } else if (bean is StandardV2RayBean) {
                             if (bean is VMessBean) {
                                 protocol = "vmess"
-                                settings = LazyOutboundConfigurationObject(
-                                    this,
+                                settings = LazyOutboundConfigurationObject(this,
                                     VMessOutboundConfigurationObject().apply {
-                                        vnext = listOf(VMessOutboundConfigurationObject.ServerObject()
-                                            .apply {
-                                                address = bean.serverAddress
-                                                port = bean.serverPort
-                                                users = listOf(VMessOutboundConfigurationObject.ServerObject.UserObject()
-                                                    .apply {
-                                                        id = bean.uuidOrGenerate()
-                                                        alterId = bean.alterId
-                                                        security = bean.encryption.takeIf { it.isNotBlank() }
-                                                            ?: "auto"
-                                                        level = 8
-                                                        experimental = ""
-                                                        if (bean.experimentalAuthenticatedLength) {
-                                                            experimental += "AuthenticatedLength"
-                                                        }
-                                                        if (bean.experimentalNoTerminationSignal) {
-                                                            experimental += "NoTerminationSignal"
-                                                        }
-                                                        if (experimental.isBlank()) experimental = null;
-                                                    })
-                                            })
+                                        vnext = listOf(
+                                            VMessOutboundConfigurationObject.ServerObject()
+                                                .apply {
+                                                    address = bean.serverAddress
+                                                    port = bean.serverPort
+                                                    users = listOf(VMessOutboundConfigurationObject.ServerObject.UserObject()
+                                                        .apply {
+                                                            id = bean.uuidOrGenerate()
+                                                            alterId = bean.alterId
+                                                            security = bean.encryption.takeIf { it.isNotBlank() }
+                                                                ?: "auto"
+                                                            level = 8
+                                                            experimental = ""
+                                                            if (bean.experimentalAuthenticatedLength) {
+                                                                experimental += "AuthenticatedLength"
+                                                            }
+                                                            if (bean.experimentalNoTerminationSignal) {
+                                                                experimental += "NoTerminationSignal"
+                                                            }
+                                                            if (experimental.isBlank()) experimental = null;
+                                                        })
+                                                })
                                     })
                             } else if (bean is VLESSBean) {
                                 protocol = "vless"
-                                settings = LazyOutboundConfigurationObject(
-                                    this,
+                                settings = LazyOutboundConfigurationObject(this,
                                     VLESSOutboundConfigurationObject().apply {
-                                        vnext = listOf(VLESSOutboundConfigurationObject.ServerObject()
-                                            .apply {
-                                                address = bean.serverAddress
-                                                port = bean.serverPort
-                                                users = listOf(VLESSOutboundConfigurationObject.ServerObject.UserObject()
-                                                    .apply {
-                                                        id = bean.uuidOrGenerate()
-                                                        encryption = bean.encryption
-                                                        level = 8
-                                                    })
-                                            })
+                                        vnext = listOf(
+                                            VLESSOutboundConfigurationObject.ServerObject()
+                                                .apply {
+                                                    address = bean.serverAddress
+                                                    port = bean.serverPort
+                                                    users = listOf(VLESSOutboundConfigurationObject.ServerObject.UserObject()
+                                                        .apply {
+                                                            id = bean.uuidOrGenerate()
+                                                            encryption = bean.encryption
+                                                            level = 8
+                                                        })
+                                                })
                                     })
                             }
 
@@ -612,14 +615,13 @@ fun buildV2RayConfig(
 
                                         if (bean.certificates.isNotBlank()) {
                                             disableSystemRoot = true
-                                            certificates = listOf(
-                                                TLSObject.CertificateObject()
-                                                    .apply {
-                                                        usage = "verify"
-                                                        certificate = bean.certificates.split(
-                                                            "\n"
-                                                        ).filter { it.isNotBlank() }
-                                                    })
+                                            certificates = listOf(TLSObject.CertificateObject()
+                                                .apply {
+                                                    usage = "verify"
+                                                    certificate = bean.certificates.split(
+                                                        "\n"
+                                                    ).filter { it.isNotBlank() }
+                                                })
                                         }
 
                                         if (bean.pinnedPeerCertificateChainSha256.isNotBlank()) {
@@ -631,6 +633,10 @@ fun buildV2RayConfig(
                                         if (bean.allowInsecure) {
                                             allowInsecure = true
                                         }
+
+                                        if (utlsFingerprint.isNotBlank()) {
+                                            fingerprint = utlsFingerprint
+                                        }
                                     }
                                 }
 
@@ -740,8 +746,7 @@ fun buildV2RayConfig(
                             }
                         } else if (bean is ShadowsocksBean) {
                             protocol = "shadowsocks"
-                            settings = LazyOutboundConfigurationObject(
-                                this,
+                            settings = LazyOutboundConfigurationObject(this,
                                 ShadowsocksOutboundConfigurationObject().apply {
                                     servers = listOf(ShadowsocksOutboundConfigurationObject.ServerObject()
                                         .apply {
@@ -760,16 +765,16 @@ fun buildV2RayConfig(
                                 })
                         } else if (bean is TrojanBean) {
                             protocol = "trojan"
-                            settings = LazyOutboundConfigurationObject(
-                                this,
+                            settings = LazyOutboundConfigurationObject(this,
                                 TrojanOutboundConfigurationObject().apply {
-                                    servers = listOf(TrojanOutboundConfigurationObject.ServerObject()
-                                        .apply {
-                                            address = bean.serverAddress
-                                            port = bean.serverPort
-                                            password = bean.password
-                                            level = 8
-                                        })
+                                    servers = listOf(
+                                        TrojanOutboundConfigurationObject.ServerObject()
+                                            .apply {
+                                                address = bean.serverAddress
+                                                port = bean.serverPort
+                                                password = bean.password
+                                                level = 8
+                                            })
                                 })
                             streamSettings = StreamSettingsObject().apply {
                                 network = "tcp"
@@ -781,6 +786,9 @@ fun buildV2RayConfig(
                                     if (bean.alpn.isNotBlank()) {
                                         alpn = bean.alpn.split("\n")
                                     }
+                                    if (utlsFingerprint.isNotBlank()) {
+                                        fingerprint = utlsFingerprint
+                                    }
                                 }
                                 if (needKeepAliveInterval) {
                                     sockopt = StreamSettingsObject.SockoptObject().apply {
@@ -837,8 +845,7 @@ fun buildV2RayConfig(
                         port = mappingPort
                         tag = "$tagOutbound-mapping-${proxyEntity.id}"
                         protocol = "dokodemo-door"
-                        settings = LazyInboundConfigurationObject(
-                            this,
+                        settings = LazyInboundConfigurationObject(this,
                             DokodemoDoorInboundConfigurationObject().apply {
                                 address = bean.serverAddress
                                 network = bean.network()
@@ -864,8 +871,7 @@ fun buildV2RayConfig(
                         port = mappingPort
                         tag = "$tagOutbound-mapping-${proxyEntity.id}"
                         protocol = "dokodemo-door"
-                        settings = LazyInboundConfigurationObject(
-                            this,
+                        settings = LazyInboundConfigurationObject(this,
                             DokodemoDoorInboundConfigurationObject().apply {
                                 address = bean.serverAddress
                                 network = bean.network()
@@ -920,8 +926,7 @@ fun buildV2RayConfig(
                     }
                     outbounds.add(0, OutboundObject().apply {
                         protocol = "loopback"
-                        settings = LazyOutboundConfigurationObject(
-                            this,
+                        settings = LazyOutboundConfigurationObject(this,
                             LoopbackOutboundConfigurationObject().apply {
                                 inboundTag = TAG_SOCKS
                             })
@@ -998,8 +1003,7 @@ fun buildV2RayConfig(
                 outbounds.add(OutboundObject().apply {
                     tag = "reverse-out-${rule.id}"
                     protocol = "freedom"
-                    settings = LazyOutboundConfigurationObject(
-                        this,
+                    settings = LazyOutboundConfigurationObject(this,
                         FreedomOutboundConfigurationObject().apply {
                             redirect = rule.redirect
                         })
@@ -1032,8 +1036,7 @@ fun buildV2RayConfig(
         for (freedom in arrayOf(TAG_DIRECT, TAG_BYPASS)) outbounds.add(OutboundObject().apply {
             tag = freedom
             protocol = "freedom"
-            settings = LazyOutboundConfigurationObject(
-                this,
+            settings = LazyOutboundConfigurationObject(this,
                 FreedomOutboundConfigurationObject().apply {
                     when (ipv6Mode) {
                         IPv6Mode.DISABLE -> domainStrategy = "UseIPv4"
@@ -1055,8 +1058,7 @@ fun buildV2RayConfig(
             listen = bind
             port = DataStore.localDNSPort
             protocol = "dokodemo-door"
-            settings = LazyInboundConfigurationObject(
-                this,
+            settings = LazyInboundConfigurationObject(this,
                 DokodemoDoorInboundConfigurationObject().apply {
                     address = if (!remoteDns.first().isIpAddress()) {
                         "1.0.0.1"
@@ -1071,8 +1073,7 @@ fun buildV2RayConfig(
         outbounds.add(OutboundObject().apply {
             protocol = "dns"
             tag = TAG_DNS_OUT
-            settings = LazyOutboundConfigurationObject(
-                this,
+            settings = LazyOutboundConfigurationObject(this,
                 DNSOutboundConfigurationObject().apply {
                     var dns = remoteDns.first()
                     if (dns.contains(":")) {
@@ -1188,8 +1189,7 @@ fun buildV2RayConfig(
                 listen = LOCALHOST
                 port = apiPort
                 tag = TAG_API_IN
-                settings = LazyInboundConfigurationObject(
-                    this,
+                settings = LazyInboundConfigurationObject(this,
                     DokodemoDoorInboundConfigurationObject().apply {
                         address = LOCALHOST
                         port = apiPort
@@ -1264,8 +1264,7 @@ fun buildCustomConfig(proxy: ProxyEntity, port: Int): V2rayBuildResult {
             listen = bind
             this.port = port
             protocol = "socks"
-            settings = LazyInboundConfigurationObject(
-                this,
+            settings = LazyInboundConfigurationObject(this,
                 SocksInboundConfigurationObject().apply {
                     auth = "noauth"
                     udp = true

+ 1 - 1
app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt

@@ -33,7 +33,7 @@ import io.nekohasekai.sagernet.ktx.*
 import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
 
 val methodsV2fly = arrayOf(
-    "none", "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305"
+    "none", "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305"
 )
 
 fun PluginConfiguration.fixInvalidParams() {

+ 2 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan/TrojanFmt.kt

@@ -26,6 +26,7 @@ import cn.hutool.json.JSONObject
 import io.nekohasekai.sagernet.IPv6Mode
 import io.nekohasekai.sagernet.database.DataStore
 import io.nekohasekai.sagernet.fmt.LOCALHOST
+import io.nekohasekai.sagernet.fmt.trojan_go.mkTrojanGoFingerprint
 import io.nekohasekai.sagernet.ktx.isIpAddress
 import io.nekohasekai.sagernet.ktx.linkBuilder
 import io.nekohasekai.sagernet.ktx.toLink
@@ -130,6 +131,7 @@ fun TrojanBean.buildTrojanGoConfig(port: Int, mux: Boolean): String {
             }
             if (sni.isNotBlank()) it["sni"] = sni
             if (alpn.isNotBlank()) it["alpn"] = JSONArray(alpn.split("\n"))
+            it["fingerprint"] = mkTrojanGoFingerprint()
         }
     }.toStringPretty()
 }

+ 11 - 2
app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoFmt.kt

@@ -138,8 +138,9 @@ fun TrojanGoBean.buildTrojanGoConfig(port: Int, mux: Boolean): String {
             sni = serverAddress
         }
 
-        if (sni.isNotBlank()) conf["ssl"] = JSONObject().also {
-            it["sni"] = sni
+        conf["ssl"] = JSONObject().also {
+            if (sni.isNotBlank()) it["sni"] = sni
+            it["fingerprint"] = mkTrojanGoFingerprint()
         }
 
         when {
@@ -227,4 +228,12 @@ fun JSONObject.parseTrojanGo(): TrojanGoBean {
             }
         }
     }
+}
+
+fun mkTrojanGoFingerprint(): String {
+    return when (val it = DataStore.utlsFingerprint) {
+        "firefox" -> it
+        "safari" -> "ios"
+        else -> "chrome"
+    }
 }

+ 1 - 0
app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java

@@ -660,6 +660,7 @@ public class V2RayConfig {
         public List<CertificateObject> certificates;
         public Boolean disableSystemRoot;
         public List<String> pinnedPeerCertificateChainSha256;
+        public String fingerprint;
 
         public static class CertificateObject {
 

File diff ditekan karena terlalu besar
+ 8 - 0
app/src/main/res/drawable/ic_baseline_fingerprint_24.xml


+ 15 - 0
app/src/main/res/values/arrays.xml

@@ -623,4 +623,19 @@
         <item>SOCKS5</item>
     </string-array>
 
+    <string-array name="utls_entry">
+        <item>DISABLED</item>
+        <item>CHROME</item>
+        <item>FIREFOX</item>
+        <item>SAFARI</item>
+        <item>RANDOMIZED</item>
+    </string-array>
+    <string-array name="utls_value">
+        <item />
+        <item>chrome</item>
+        <item>firefox</item>
+        <item>safari</item>
+        <item>randomized</item>
+    </string-array>
+
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -402,4 +402,5 @@
     <string name="experimental_settings">Experimental</string>
     <string name="experimental_authenticated_length">Make the length of each payload segment no longer malleable. This experiment requires the server and client use the same version of v2ray-core. More breaking updates on this experiment is expected.</string>
     <string name="experimental_no_termination_signal">Do not send connection single duplex termination signal for TCP connection when transferred over VMess. This will break some application.</string>
+    <string name="utls_fingerprint">uTLS Fingerprint</string>
 </resources>

+ 8 - 0
app/src/main/res/xml/global_preferences.xml

@@ -260,6 +260,14 @@
     </PreferenceCategory>
 
     <PreferenceCategory app:title="@string/cag_misc">
+        <com.takisoft.preferencex.SimpleMenuPreference
+            app:defaultValue=""
+            app:entries="@array/utls_entry"
+            app:entryValues="@array/utls_value"
+            app:icon="@drawable/ic_baseline_fingerprint_24"
+            app:key="utlsFingerprint"
+            app:title="@string/utls_fingerprint"
+            app:useSimpleSummaryProvider="true" />
         <io.nekohasekai.sagernet.widget.LinkPreference
             app:defaultValue="https://api.v2fly.org/checkConnection.svgz"
             app:icon="@drawable/ic_baseline_cast_connected_24"

+ 1 - 1
external/v2ray-core

@@ -1 +1 @@
-Subproject commit e2d526cd8b4d5170d9dcfa2a0ce1f31c6fbd5c8b
+Subproject commit 6121c3e697e358c027641d810d4d9c02afd438cb

+ 1 - 1
library/libcore

@@ -1 +1 @@
-Subproject commit 4ab703f81862c54b83692965a999e82fe57767da
+Subproject commit a6a0922389d073c9e5661ba997262a84d9093ad2

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini